wcs-time-functions.php 22.3 KB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629
<?php
/**
 * WooCommerce Subscriptions Temporal Functions
 *
 * Functions for time values and ranges
 *
 * @author 		Prospress
 * @category 	Core
 * @package 	WooCommerce Subscriptions/Functions
 * @version     2.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}


/**
 * Return an i18n'ified associative array of all possible subscription periods.
 *
 * @param int (optional) An interval in the range 1-6
 * @param string (optional) One of day, week, month or year. If empty, all subscription ranges are returned.
 * @since 2.0
 */
function wcs_get_subscription_period_strings( $number = 1, $period = '' ) {

	$translated_periods = apply_filters( 'woocommerce_subscription_periods',
		array(
			// translators: placeholder is number of days. (e.g. "Bill this every day / 4 days")
			'day'   => sprintf( _nx( 'day',   '%s days',   $number, 'Subscription billing period.', 'woocommerce-subscriptions' ), $number ),
			// translators: placeholder is number of weeks. (e.g. "Bill this every week / 4 weeks")
			'week'  => sprintf( _nx( 'week',  '%s weeks',  $number, 'Subscription billing period.', 'woocommerce-subscriptions' ), $number ),
			// translators: placeholder is number of months. (e.g. "Bill this every month / 4 months")
			'month' => sprintf( _nx( 'month', '%s months', $number, 'Subscription billing period.', 'woocommerce-subscriptions' ), $number ),
			// translators: placeholder is number of years. (e.g. "Bill this every year / 4 years")
			'year'  => sprintf( _nx( 'year',  '%s years',  $number, 'Subscription billing period.', 'woocommerce-subscriptions' ), $number ),
		)
	);

	return ( ! empty( $period ) ) ? $translated_periods[ $period ] : $translated_periods;
}

/**
 * Return an i18n'ified associative array of all possible subscription trial periods.
 *
 * @param int (optional) An interval in the range 1-6
 * @param string (optional) One of day, week, month or year. If empty, all subscription ranges are returned.
 * @since 2.0
 */
function wcs_get_subscription_trial_period_strings( $number = 1, $period = '' ) {

	$translated_periods = apply_filters( 'woocommerce_subscription_trial_periods',
		array(
			'day'   => sprintf( _n( '%s day', 'a %s-day', $number, 'woocommerce-subscriptions' ), $number ),
			'week'  => sprintf( _n( '%s week', 'a %s-week', $number, 'woocommerce-subscriptions' ), $number ),
			'month' => sprintf( _n( '%s month', 'a %s-month', $number, 'woocommerce-subscriptions' ), $number ),
			'year'  => sprintf( _n( '%s year', 'a %s-year', $number, 'woocommerce-subscriptions' ), $number ),
		)
	);

	return ( ! empty( $period ) ) ? $translated_periods[ $period ] : $translated_periods;
}

/**
 * Returns an array of subscription lengths.
 *
 * PayPal Standard Allowable Ranges
 * D – for days; allowable range is 1 to 90
 * W – for weeks; allowable range is 1 to 52
 * M – for months; allowable range is 1 to 24
 * Y – for years; allowable range is 1 to 5
 *
 * @param string (optional) One of day, week, month or year. If empty, all subscription ranges are returned.
 * @since 2.0
 */
function wcs_get_subscription_ranges_tlc() {

	foreach ( array( 'day', 'week', 'month', 'year' ) as $period ) {

		$subscription_lengths = array(
			_x( 'all time', 'Subscription length (eg "$10 per month for _all time_")', 'woocommerce-subscriptions' ),
		);

		switch ( $period ) {
			case 'day':
				$subscription_lengths[] = _x( '1 day', 'Subscription lengths. e.g. "For 1 day..."', 'woocommerce-subscriptions' );
				$subscription_range = range( 2, 90 );
				break;
			case 'week':
				$subscription_lengths[] = _x( '1 week', 'Subscription lengths. e.g. "For 1 week..."', 'woocommerce-subscriptions' );
				$subscription_range = range( 2, 52 );
				break;
			case 'month':
				$subscription_lengths[] = _x( '1 month', 'Subscription lengths. e.g. "For 1 month..."', 'woocommerce-subscriptions' );
				$subscription_range = range( 2, 24 );
				break;
			case 'year':
				$subscription_lengths[] = _x( '1 year', 'Subscription lengths. e.g. "For 1 year..."', 'woocommerce-subscriptions' );
				$subscription_range = range( 2, 5 );
				break;
		}

		foreach ( $subscription_range as $number ) {
			$subscription_range[ $number ] = wcs_get_subscription_period_strings( $number, $period );
		}

		// Add the possible range to all time range
		$subscription_lengths += $subscription_range;

		$subscription_ranges[ $period ] = $subscription_lengths;
	}

	return $subscription_ranges;
}

/**
 * Retaining the API, it makes use of the transient functionality.
 *
 * @param string $period
 * @return bool|mixed
 */
function wcs_get_subscription_ranges( $subscription_period = '' ) {
	if ( ! is_string( $subscription_period ) ) {
		$subscription_period = '';
	}

	$locale = get_locale();

	$subscription_ranges = WC_Subscriptions::$cache->cache_and_get( 'wcs-sub-ranges-' . $locale, 'wcs_get_subscription_ranges_tlc', array(), 3 * HOUR_IN_SECONDS );

	$subscription_ranges = apply_filters( 'woocommerce_subscription_lengths', $subscription_ranges, $subscription_period );

	if ( ! empty( $subscription_period ) ) {
		return $subscription_ranges[ $subscription_period ];
	} else {
		return $subscription_ranges;
	}
}

/**
 * Return an i18n'ified associative array of all possible subscription periods.
 *
 * @param int (optional) An interval in the range 1-6
 * @since 2.0
 */
function wcs_get_subscription_period_interval_strings( $interval = '' ) {

	$intervals = array( 1 => _x( 'every', 'period interval (eg "$10 _every_ 2 weeks")', 'woocommerce-subscriptions' ) );

	foreach ( range( 2, 6 ) as $i ) {
		// translators: period interval, placeholder is ordinal (eg "$10 every _2nd/3rd/4th_", etc)
		$intervals[ $i ] = sprintf( _x( 'every %s', 'period interval with ordinal number (e.g. "every 2nd"', 'woocommerce-subscriptions' ), WC_Subscriptions::append_numeral_suffix( $i ) );
	}

	$intervals = apply_filters( 'woocommerce_subscription_period_interval_strings', $intervals );

	if ( empty( $interval ) ) {
		return $intervals;
	} else {
		return $intervals[ $interval ];
	}
}

/**
 * Return an i18n'ified associative array of all time periods allowed for subscriptions.
 *
 * @param string (Optional) Either 'singular' for singular trial periods or 'plural'.
 * @since 2.0
 */
function wcs_get_available_time_periods( $form = 'singular' ) {

	$number = ( 'singular' === $form ) ? 1 : 2;

	$translated_periods = apply_filters( 'woocommerce_subscription_available_time_periods',
		array(
			'day'   => _nx( 'day',   'days',   $number, 'Used in the trial period dropdown. Number is in text field. 0, 2+ will need plural, 1 will need singular.', 'woocommerce-subscriptions' ),
			'week'  => _nx( 'week',  'weeks',  $number, 'Used in the trial period dropdown. Number is in text field. 0, 2+ will need plural, 1 will need singular.', 'woocommerce-subscriptions' ),
			'month' => _nx( 'month', 'months', $number, 'Used in the trial period dropdown. Number is in text field. 0, 2+ will need plural, 1 will need singular.', 'woocommerce-subscriptions' ),
			'year'  => _nx( 'year',  'years',  $number, 'Used in the trial period dropdown. Number is in text field. 0, 2+ will need plural, 1 will need singular.', 'woocommerce-subscriptions' ),
		)
	);

	return $translated_periods;
}

/**
 * Returns an array of allowed trial period lengths.
 *
 * @param string (optional) One of day, week, month or year. If empty, all subscription trial period lengths are returned.
 * @since 2.0
 */
function wcs_get_subscription_trial_lengths( $subscription_period = '' ) {

	$all_trial_periods = wcs_get_subscription_ranges();

	foreach ( $all_trial_periods as $period => $trial_periods ) {
		$all_trial_periods[ $period ][0] = _x( 'no', 'no trial period', 'woocommerce-subscriptions' );
	}

	if ( ! empty( $subscription_period ) ) {
		return $all_trial_periods[ $subscription_period ];
	} else {
		return $all_trial_periods;
	}
}

/**
 * Convenience wrapper for adding "{n} {periods}" to a timestamp (e.g. 2 months or 5 days).
 *
 * @param int The number of periods to add to the timestamp
 * @param string One of day, week, month or year.
 * @param int A Unix timestamp to add the time too.
 * @since 2.0
 */
function wcs_add_time( $number_of_periods, $period, $from_timestamp ) {

	if ( 'month' == $period ) {
		$next_timestamp = wcs_add_months( $from_timestamp, $number_of_periods );
	} else {
		$next_timestamp = strtotime( "+ {$number_of_periods} {$period}", $from_timestamp );
	}

	return $next_timestamp;
}

/**
 * Workaround the last day of month quirk in PHP's strtotime function.
 *
 * Adding +1 month to the last day of the month can yield unexpected results with strtotime().
 * For example:
 * - 30 Jan 2013 + 1 month = 3rd March 2013
 * - 28 Feb 2013 + 1 month = 28th March 2013
 *
 * What humans usually want is for the date to continue on the last day of the month.
 *
 * @param int $from_timestamp A Unix timestamp to add the months too.
 * @param int $months_to_add The number of months to add to the timestamp.
 * @since 2.0
 */
function wcs_add_months( $from_timestamp, $months_to_add ) {

	$first_day_of_month = date( 'Y-m', $from_timestamp ) . '-1';
	$days_in_next_month = date( 't', strtotime( "+ {$months_to_add} month", strtotime( $first_day_of_month ) ) );

	// Payment is on the last day of the month OR number of days in next billing month is less than the the day of this month (i.e. current billing date is 30th January, next billing date can't be 30th February)
	if ( date( 'd m Y', $from_timestamp ) === date( 't m Y', $from_timestamp ) || date( 'd', $from_timestamp ) > $days_in_next_month ) {
		for ( $i = 1; $i <= $months_to_add; $i++ ) {
			$next_month = strtotime( '+ 3 days', $from_timestamp ); // Add 3 days to make sure we get to the next month, even when it's the 29th day of a month with 31 days
			$next_timestamp = $from_timestamp = strtotime( date( 'Y-m-t H:i:s', $next_month ) ); // NB the "t" to get last day of next month
		}
	} else { // Safe to just add a month
		$next_timestamp = strtotime( "+ {$months_to_add} month", $from_timestamp );
	}

	return $next_timestamp;
}

/**
 * Estimate how many days, weeks, months or years there are between now and a given
 * date in the future. Estimates the minimum total of periods.
 *
 * @param int $start_timestamp A Unix timestamp
 * @param int $end_timestamp A Unix timestamp at some time in the future
 * @param string $end_timestamp A unit of time, either day, week month or year.
 * @param string $unit_of_time A rounding method, either ceil (default) or floor for anything else
 * @since 2.0
 */
function wcs_estimate_periods_between( $start_timestamp, $end_timestamp, $unit_of_time = 'month', $rounding_method = 'ceil' ) {

	if ( $end_timestamp <= $start_timestamp ) {

		$periods_until = 0;

	} elseif ( 'month' == $unit_of_time ) {

		// Calculate the number of times this day will occur until we'll be in a time after the given timestamp
		$timestamp = $start_timestamp;

		if ( 'ceil' == $rounding_method ) {
			for ( $periods_until = 0; $timestamp < $end_timestamp; $periods_until++ ) {
				$timestamp = wcs_add_months( $timestamp, 1 );
			}
		} else {
			for ( $periods_until = -1; $timestamp <= $end_timestamp; $periods_until++ ) {
				$timestamp = wcs_add_months( $timestamp, 1 );
			}
		}
	} else {

		$seconds_until_timestamp = $end_timestamp - $start_timestamp;

		switch ( $unit_of_time ) {

			case 'day' :
				$denominator = DAY_IN_SECONDS;
				break;

			case 'week' :
				$denominator = WEEK_IN_SECONDS;
				break;

			case 'year' :
				$denominator = YEAR_IN_SECONDS;
				// we need to adjust this because YEAR_IN_SECONDS assumes a 365 day year. See notes on wcs_number_of_leap_days
				$seconds_until_timestamp = $seconds_until_timestamp - wcs_number_of_leap_days( $start_timestamp, $end_timestamp ) * DAY_IN_SECONDS;
				break;
		}

		$periods_until = ( 'ceil' == $rounding_method ) ? ceil( $seconds_until_timestamp / $denominator ) : floor( $seconds_until_timestamp / $denominator );
	}

	return $periods_until;
}

/**
 * Utility function to find out how many leap days are there between two given dates. The reason we need this is because
 * the constant YEAR_IN_SECONDS assumes a 365 year, which means some of the calculations are going to be off by a day.
 * This has caused problems where if there's a leap year, wcs_estimate_periods_between would return 2 years instead of
 * 1, making certain payments wildly inaccurate.
 *
 * @param int $start_timestamp A unix timestamp
 * @param int $end_timestamp A unix timestamp
 *
 * @return int number of leap days between the start and end timstamps
 */
function wcs_number_of_leap_days( $start_timestamp, $end_timestamp ) {
	if ( ! is_int( $start_timestamp ) || ! is_int( $end_timestamp ) ) {
		throw new InvalidArgumentException( 'Start or end times are not integers' );
	}
	// save the date! ;)
	$default_tz = date_default_timezone_get();
	date_default_timezone_set( 'UTC' );

	// Years to check
	$years = range( date( 'Y', $start_timestamp ), date( 'Y', $end_timestamp ) );
	$leap_years = array_filter( $years, 'wcs_is_leap_year' );
	$total_feb_29s = 0;

	if ( ! empty( $leap_years ) ) {
		// Let's get the first feb 29 in the list
		$first_feb_29 = mktime( 23, 59, 59, 2, 29, reset( $leap_years ) );
		$last_feb_29 = mktime( 0, 0, 0, 2, 29, end( $leap_years ) );

		$is_first_feb_covered = ( $first_feb_29 >= $start_timestamp ) ? 1: 0;
		$is_last_feb_covered = ( $last_feb_29 <= $end_timestamp ) ? 1: 0;

		if ( count( $leap_years ) > 1 ) {
			// the feb 29s are in different years
			$total_feb_29s = count( $leap_years ) - 2 + $is_first_feb_covered + $is_last_feb_covered;
		} else {
			$total_feb_29s = ( $first_feb_29 >= $start_timestamp && $last_feb_29 <= $end_timestamp ) ? 1: 0;
		}
	}
	date_default_timezone_set( $default_tz );

	return $total_feb_29s;
}

/**
 * Filter function used in wcs_number_of_leap_days
 *
 * @param $year int A four digit year, eg 2017
 *
 * @return bool|string
 */
function wcs_is_leap_year( $year ) {
	return date( 'L', mktime( 0, 0, 0, 1, 1, $year ) );
}
/**
 * Method to try to determine the period of subscriptions if data is missing. It tries the following, in order:
 *
 * - defaults to month
 * - comes up with an array of possible values given the standard time spans (day / week / month / year)
 * - ranks them
 * - discards 0 interval values
 * - discards high deviation values
 * - tries to match with passed in interval
 * - if all else fails, sorts by interval and returns the one having the lowest interval, or the first, if equal (that should
 *   not happen though)
 *
 * @param  string  $last_date   mysql date string
 * @param  string  $second_date mysql date string
 * @param  integer $interval    potential interval
 * @return string               period string
 */
function wcs_estimate_period_between( $last_date, $second_date, $interval = 1 ) {

	if ( ! is_int( $interval ) ) {
		$interval = 1;
	}

	$last_timestamp    = strtotime( $last_date );
	$second_timestamp  = strtotime( $second_date );

	$earlier_timestamp = min( $last_timestamp, $second_timestamp );
	$later_timestamp   = max( $last_timestamp, $second_timestamp );

	$days_in_month     = date( 't', $earlier_timestamp );
	$difference        = absint( $last_timestamp - $second_timestamp );
	$period_in_seconds = round( $difference / $interval );
	$possible_periods  = array();

	// check for months
	$full_months = wcs_find_full_months_between( $earlier_timestamp, $later_timestamp );

	$possible_periods['month'] = array(
		'intervals'         => $full_months['months'],
		'remainder'         => $remainder = $full_months['remainder'],
		'fraction'          => $remainder / ( 30 * DAY_IN_SECONDS ),
		'period'            => 'month',
		'days_in_month'     => $days_in_month,
		'original_interval' => $interval,
	);

	// check for different time spans
	foreach ( array( 'year' => YEAR_IN_SECONDS, 'week' => WEEK_IN_SECONDS, 'day' => DAY_IN_SECONDS ) as $time => $seconds ) {
		$possible_periods[ $time ] = array(
			'intervals'         => floor( $period_in_seconds / $seconds ),
			'remainder'         => $remainder = $period_in_seconds % $seconds,
			'fraction'          => $remainder / $seconds,
			'period'            => $time,
			'days_in_month'     => $days_in_month,
			'original_interval' => $interval,
		);
	}

	// filter out ones that are less than one period
	$possible_periods_zero_filtered = array_filter( $possible_periods, 'wcs_discard_zero_intervals' );
	if ( empty( $possible_periods_zero_filtered ) ) {
		// fall back if the difference is less than a day and return default 'day'
		return 'day';
	} else {
		$possible_periods = $possible_periods_zero_filtered;
	}

	// filter out ones that have too high of a deviation
	$possible_periods_no_hd = array_filter( $possible_periods, 'wcs_discard_high_deviations' );

	if ( count( $possible_periods_no_hd ) == 1 ) {
		// only one matched, let's return that as our best guess
		$possible_periods_no_hd = array_shift( $possible_periods_no_hd );
		return $possible_periods_no_hd['period'];
	} elseif ( count( $possible_periods_no_hd ) > 1 ) {
		$possible_periods = $possible_periods_no_hd;
	}

	// check for interval equality
	$possible_periods_interval_match = array_filter( $possible_periods, 'wcs_match_intervals' );

	if ( count( $possible_periods_interval_match ) == 1 ) {
		foreach ( $possible_periods_interval_match as $period_data ) {
			// only one matched the interval as our best guess
			return $period_data['period'];
		}
	} elseif ( count( $possible_periods_interval_match ) > 1 ) {
		$possible_periods = $possible_periods_interval_match;
	}

	// order by number of intervals and return the lowest

	usort( $possible_periods, 'wcs_sort_by_intervals' );

	$least_interval = array_shift( $possible_periods );

	return $least_interval['period'];
}

/**
 * Finds full months between two dates and the remaining seconds after the end of the last full month. Takes into account
 * leap years and variable number of days in months. Uses wcs_add_months
 *
 * @param  numeric $start_timestamp unix timestamp of a start date
 * @param  numeric $end_timestamp   unix timestamp of an end date
 * @return array                    with keys 'months' (integer) and 'remainder' (seconds, integer)
 */
function wcs_find_full_months_between( $start_timestamp, $end_timestamp ) {
	$number_of_months = 0;
	$remainder = null;
	$previous_remainder = null;

	while ( 0 <= $remainder ) {
		$previous_timestamp = $start_timestamp;
		$start_timestamp = wcs_add_months( $start_timestamp, 1 );
		$previous_remainder = $remainder;
		$remainder = $end_timestamp - $start_timestamp;

		if ( $remainder >= 0 ) {
			$number_of_months++;
		} elseif ( null === $previous_remainder ) {
			$previous_remainder = $end_timestamp - $previous_timestamp;
		}
	}

	$time_difference = array(
		'months' => $number_of_months,
		'remainder' => $previous_remainder,
	);

	return $time_difference;
}

/**
 * Used in an array_filter, removes elements where intervals are less than 0
 *
 * @param  array $array elements of an array
 * @return bool        true if at least 1 interval
 */
function wcs_discard_zero_intervals( $array ) {
	return $array['intervals'] > 0;
}

/**
 * Used in an array_filter, discards high deviation elements.
 * - for days it's 1/24th
 * - for week it's 1/7th
 * - for year it's 1/300th
 * - for month it's 1/($days_in_months-2)
 *
 * @param  array $array elements of the filtered array
 * @return bool        true if value is within deviation limit
 */
function wcs_discard_high_deviations( $array ) {
	switch ( $array['period'] ) {
		case 'year':
			return $array['fraction'] < ( 1 / 300 );
			break;
		case 'month':
			return $array['fraction'] < ( 1 / ( $array['days_in_month'] - 2 ) );
			break;
		case 'week':
			return $array['fraction'] < ( 1 / 7 );
			break;
		case 'day':
			return $array['fraction'] < ( 1 / 24 );
			break;
		default:
			return false;
	}
}

/**
 * Used in an array_filter, tries to match intervals against passed in interval
 * @param  array $array elements of filtered array
 * @return bool        true if intervals match
 */
function wcs_match_intervals( $array ) {
	return $array['intervals'] == $array['original_interval'];
}

/**
 * Used in a usort, responsible for making sure the array is sorted in ascending order by intervals
 *
 * @param  array $a one element of the sorted array
 * @param  array $b different element of the sorted array
 * @return int    0 if equal, -1 if $b is larger, 1 if $a is larger
 */
function wcs_sort_by_intervals( $a, $b ) {
	if ( $a['intervals'] == $b['intervals'] ) {
		return 0;
	}
	return ( $a['intervals'] < $b['intervals'] ) ? -1 : 1;
}

/**
 * Used in a usort, responsible for making sure the array is sorted in descending order by fraction.
 *
 * @param  array $a one element of the sorted array
 * @param  array $b different element of the sorted array
 * @return int    0 if equal, -1 if $b is larger, 1 if $a is larger
 */
function wcs_sort_by_fractions( $a, $b ) {
	if ( $a['fraction'] == $b['fraction'] ) {
		return 0;
	}
	return ( $a['fraction'] > $b['fraction'] ) ? -1 : 1;
}

/**
 * PHP on Windows does not have strptime function. Therefore this is what we're using to check
 * whether the given time is of a specific format.
 *
 * @param  string $time the mysql time string
 * @return boolean      true if it matches our mysql pattern of YYYY-MM-DD HH:MM:SS
 */
function wcs_is_datetime_mysql_format( $time ) {
	if ( ! is_string( $time ) ) {
		return false;
	}

	if ( function_exists( 'strptime' ) ) {
		$valid_time = $match = ( false !== strptime( $time, '%Y-%m-%d %H:%M:%S' ) ) ? true : false;
	} else {
		// parses for the pattern of YYYY-MM-DD HH:MM:SS, but won't check whether it's a valid timedate
		$match = preg_match( '/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/', $time );

		// parses time, returns false for invalid dates
		$valid_time = strtotime( $time );
	}

	// magic number -2209078800 is strtotime( '1900-01-00 00:00:00' ). Needed to achieve parity with strptime
	return ( $match && false !== $valid_time && -2209078800 <= $valid_time ) ? true : false;
}

/**
 * Find the average number of days for a given billing period and interval.
 *
 * @param  string $period a billing period: day, week, month or year.
 * @param  int $interval a billing interval
 * @return int the number of days in that billing cycle
 */
function wcs_get_days_in_cycle( $period, $interval ) {

	switch ( $period ) {
		case 'day' :
			$days_in_cycle = $interval;
			break;
		case 'week' :
			$days_in_cycle = $interval * 7;
			break;
		case 'month' :
			$days_in_cycle = $interval * 30.4375; // Average days per month over 4 year period
			break;
		case 'year' :
			$days_in_cycle = $interval * 365.25; // Average days per year over 4 year period
			break;
	}

	return apply_filters( 'wcs_get_days_in_cycle', $days_in_cycle, $period, $interval );
}