/[drupal]/contributions/modules/duration/duration.inc
ViewVC logotype

Contents of /contributions/modules/duration/duration.inc

Parent Directory Parent Directory | Revision Log Revision Log | View Revision Graph Revision Graph


Revision 1.7 - (show annotations) (download) (as text)
Sun Oct 26 14:08:37 2008 UTC (13 months ago) by jpetso
Branch: MAIN
CVS Tags: DRUPAL-6--1-0, HEAD
Changes since 1.6: +16 -16 lines
File MIME type: text/x-php
#316940: Fix named pattern syntax in regular expressions.
1 <?php
2 // $Id: duration.inc,v 1.6 2008/07/17 17:27:47 jpetso Exp $
3
4 /**
5 * @file
6 * An API to transform and perform evaluations on duration objects.
7 *
8 * This file contains the duration class, featuring methods for transformation
9 * from and to ISO 8601 compliant duration strings and some more essential
10 * goodness for your duration handling pleasure.
11 *
12 * Copyright 2008 by Jakob Petsovits <jpetso@gmx.at>
13 * Distributed under the GNU General Public Licence version 2 or higher,
14 * as published by the FSF on http://www.gnu.org/copyleft/gpl.html
15 */
16
17 /**
18 * Return a new duration object.
19 *
20 * @param $duration_iso_string
21 * A duration's string representation, as defined by the ISO 8601 standard.
22 * If this is left unset, a duration with zero length will be created.
23 *
24 * @return
25 * A valid duration object if the string did actually conform to the
26 * ISO 8601 duration format, or NULL if it didn't and the duration object
27 * is therefore invalid.
28 */
29 function duration_create($duration_iso_string = NULL) {
30 $duration = new Duration($duration_iso_string);
31 return ($duration->is_valid() ? $duration : NULL);
32 }
33
34
35 class Duration {
36
37 // An array containing the members 'seconds', 'minutes', 'hours', 'days',
38 // either 'weeks' or 'months', and 'years'.
39 // The 'week' and 'month' formats are mutually exclusive; in case of doubt,
40 // we use the 'month' format.
41 var $duration;
42
43 // Boolean, specifying whether the duration is negative (TRUE) or not (FALSE).
44 var $is_negative;
45
46 // Boolean, set to FALSE if the duration string could not be parsed,
47 // otherwise TRUE.
48 var $is_valid;
49
50 var $conversion_factors;
51
52 /**
53 * Create a new duration object.
54 *
55 * @param $duration_iso_string
56 * A duration's string representation, as defined by the ISO 8601 standard.
57 * If this is left unset, a duration with zero length will be created.
58 */
59 function __construct($duration_iso_string = NULL) {
60 // Easy way: no string given, let's just initialize a zero duration.
61 if (!isset($duration_iso_string)) {
62 $this->is_negative = FALSE;
63 $this->duration = array();
64 $this->is_valid = TRUE;
65 return;
66 }
67
68 // Slightly more demanding version: there's a parsable ISO string given.
69 // This is a bit complex, let's construct the regexp step by step.
70 $this->is_valid = FALSE;
71
72 // Durations can also be negative (but not single parts of the duration).
73 $sign = '(?P<sign>\\-)?';
74
75 // For the format with designators, numbers can contain decimal separators
76 // and arbitrary lengths.
77 $number = '(?:\d+(?:[,.]\d+)?)';
78 $years = "(?:(?P<years>${number})Y)";
79 $months = "(?:(?P<months>${number})M)";
80 $weeks = "(?:(?P<weeks>${number})W)";
81 $days = "(?:(?P<days>${number})D)";
82 $hours = "(?:(?P<hours>${number})H)";
83 $minutes = "(?:(?P<minutes>${number})M)";
84 $seconds = "(?:(?P<seconds>${number})S)";
85
86 // So the format with designators looks like this:
87 // (example: "P3Y6M4DT12H30M0S")
88 $des_datepart = "(?:${years}?(?:${months}|${weeks})?${days}?)";
89 $des_timepart = "(?:T${hours}?${minutes}?${seconds}?)";
90 $des_datetime = "/^${sign}P(?:${des_datepart}?${des_timepart}?)$/";
91
92 // For the alternative format, numbers have fixed length and no designator.
93 $years = "(?P<years>\d\d\d\d)";
94 $months = "(?P<months>\d\d)";
95 $days = "(?P<days>\d\d)"; // with months: two digits
96 $days_only = "(?P<daysonly>\d\d\d)"; // without months: three digits
97 $hours = "(?P<hours>\d\d)";
98 $minutes = "(?P<minutes>\d\d)";
99 $seconds = "(?P<seconds>\d\d)";
100
101 // The alternative format is available in basic form
102 // (example: "P00030604T123000")...
103 $alt_datepart = "(?:${years}(?:${months}${days}|${days_only}))";
104 $alt_timepart = "(?:T${hours}${minutes}${seconds})";
105 $alt_datetime = "/^${sign}P(?:${alt_datepart}${alt_timepart})$/";
106
107 // ...or in extended form (example: "P0003-06-04T12:30:00").
108 $alt_datepart_ext = "(?:${years}\\-(?:${months}\\-${days}|${days_only}))";
109 $alt_timepart_ext = "(?:T${hours}:${minutes}:${seconds})";
110 $alt_datetime_ext = "/^${sign}P(?:${alt_datepart_ext}${alt_timepart_ext})$/";
111
112 // Ok, regexp is complete, now let's check if it matches and get the values!
113 if (preg_match($des_datetime, $duration_iso_string, $matches)
114 || preg_match($alt_datetime, $duration_iso_string, $matches)
115 || preg_match($alt_datetime_ext, $duration_iso_string, $matches)) {
116 $this->is_negative = !empty($matches['sign']);
117 $this->duration = array(); // to be filled right now
118
119 foreach ($this->_allowed_metrics() as $metric) {
120 if (strlen($matches[$metric]) > 0) {
121 $this->duration[$metric] = floatval($matches[$metric]);
122 }
123 }
124 if (strlen($this->duration['daysonly']) > 0) {
125 $this->duration['days'] = $this->duration['daysonly'];
126 $this->duration['weeks'] = 0; // 'Tyyyy-ddd(...)' indicates a y/w/d format
127 unset($this->duration['daysonly']);
128 }
129 }
130 if (!empty($this->duration)) {
131 $this->_sanitize();
132 $this->is_valid = TRUE;
133 }
134 }
135
136 /**
137 * Determine if this duration object is valid, i.e. the ISO 8601 string was
138 * successfully be parsed and further calculations can be done.
139 * If this method returns FALSE, the behaviour and result of any other
140 * methods of this object is undefined.
141 */
142 function is_valid() {
143 return $this->is_valid;
144 }
145
146 /**
147 * Durations may include a combination of months and days, or a combination
148 * of weeks and days. Those two are mutually exclusive and will lead to
149 * incorrect calculations when they are mixed, so better ask this method
150 * in order to determine in which format this duration is stored.
151 *
152 * @return
153 * Either 'weeks' if the duration includes a week specifier,
154 * or 'months' otherwise.
155 */
156 function type() {
157 return isset($this->duration['weeks']) ? 'weeks' : 'months';
158 }
159
160 /**
161 * Ensure that this duration uses the specified format, which is either
162 * 'months' or 'weeks'. See type() for an explanation of this duality.
163 *
164 * At the moment, this conversion is rather naive - it just sets the
165 * new metric (if the type is being changed at all) to 0, and unsets
166 * the other, mutually exclusive metric. In the future this might be
167 * an algorithm that preserves existing values more accurately.
168 */
169 function set_type($type) {
170 $current_type = $this->type;
171
172 if ($type == 'weeks' && $current_type != 'weeks') {
173 $this->set_weeks(0);
174 }
175 if ($type == 'months' && $current_type != 'months') {
176 $this->set_months(0);
177 }
178 }
179
180
181 function is_negative() {
182 return $this->is_negative;
183 }
184
185 /**
186 * Define if the duration is negative (i.e. adding it to a date produces an
187 * earlier date) or positive (i.e. adding it produces a future date).
188 * The duration values themselves are not being changed, this function
189 * just sets the minus sign (or not).
190 *
191 * @param $is_negative
192 * TRUE if the duration should be negative,
193 * or FALSE if it should be positive.
194 */
195 function set_negative($is_negative) {
196 $this->is_negative = $is_negative;
197 }
198
199
200 /**
201 * Retrieve the 'years' value of this duration.
202 */
203 function get_years() {
204 return $this->get_value('years');
205 }
206
207 /**
208 * Retrieve the 'months' value of this duration.
209 */
210 function get_months() {
211 return $this->get_value('months');
212 }
213
214 /**
215 * Retrieve the 'weeks' value of this duration.
216 *
217 * @param $allow_null_result
218 * If TRUE, this method will return NULL if the 'weeks' value is not set,
219 * i.e. if it won't normally appear in formatted output values.
220 * If FALSE, this method will always return a number.
221 */
222 function get_weeks() {
223 return $this->get_value('weeks');
224 }
225
226 /**
227 * Retrieve the 'days' value of this duration.
228 */
229 function get_days() {
230 return $this->get_value('days');
231 }
232
233 /**
234 * Retrieve the 'hours' value of this duration.
235 */
236 function get_hours() {
237 return $this->get_value('hours');
238 }
239
240 /**
241 * Retrieve the 'minutes' value of this duration.
242 */
243 function get_minutes() {
244 return $this->get_value('minutes');
245 }
246
247 /**
248 * Retrieve the 'seconds' value of this duration.
249 */
250 function get_seconds() {
251 return $this->get_value('seconds');
252 }
253
254 /**
255 * Retrieve the value of the given metric.
256 *
257 * @param $metric
258 * The metric to be retrieved. Possible values: 'seconds', 'minutes',
259 * 'hours', 'days', 'weeks', 'months' and 'years'.
260 */
261 function get_value($metric) {
262 return isset($this->duration[$metric]) ? $this->duration[$metric] : 0;
263 }
264
265
266 /**
267 * Retrieve an array including all metrics as array keys together with their
268 * corresponding values.
269 *
270 * @param $sort
271 * The order in which the metrics should be sorted:
272 * 'descending' for "years first, seconds last",
273 * and 'ascending' for "seconds first, years last".
274 */
275 function to_array($sort = 'descending') {
276 $duration = array();
277 foreach ($this->_metrics($sort) as $metric => $info) {
278 $duration[$metric] = $this->get_value($metric);
279 }
280 return $duration;
281 }
282
283 /**
284 * Return the length of this duration as a single value, in the given metric.
285 * For example, you can use this function to retrieve the duration length
286 * as seconds, hours, or years.
287 *
288 * Note that the 'months' and 'years' metrics are likely to cause inaccurate
289 * results because months and years have differences in length depending on
290 * which month or year this applies to. As an approximization, months are
291 * calculated by using a conversion factor of 30 days, and (365 / 7)
292 * is used for weeks in a year.
293 *
294 * Possible values for @p $metric: 'seconds', 'minutes', 'hours',
295 * 'days', 'weeks' (only if type() returns 'weeks'), 'months' (only if type()
296 * returns 'months'), and 'years'.
297 */
298 function to_single_metric($metric) {
299 $duration = $this->duration; // backup the original values
300
301 // Calculate by transforming all larger and smaller metrics to $metric.
302 $this->set_granularity($metric, $metric);
303 $value = $this->get_value($metric);
304 if ($this->is_negative()) {
305 $value *= -1;
306 }
307
308 $this->duration = $duration; // restore the original values
309 return $value;
310 }
311
312
313 /**
314 * Return a duration string formatted according to the given ISO 8601 format.
315 *
316 * @param $format
317 * Can be 'designators' (default, for the "format with designators"),
318 * 'alternative_basic' (for the alternative format without delimiter signs)
319 * or 'alternative_extended' (alternative format with delimiter signs).
320 *
321 * Note that when using one of the 'alternative' formats, values higher
322 * than the maximum value of each metric (12 months per year, 24 hours
323 * per day, etc.) will be broken down to the next smaller unit so that
324 * there is no overflow. This behaviour also causes weeks to disappear
325 * in favor of days that are used instead, see the ISO 8601 standard
326 * for more information on this.
327 *
328 * For accurate preservation of the internal object data,
329 * the 'designators' format is the recommended one.
330 */
331 function to_iso($format = 'designators') {
332 if ($format == 'alternative_basic' || $format == 'alternative_extended') {
333 $date_delimiter = ($format == 'alternative_extended') ? '-' : '';
334 $time_delimiter = ($format == 'alternative_extended') ? ':' : '';
335
336 // Backup the current duration (and restore it later), because we
337 // don't want the changes in here to be stored permanently.
338 $duration = $this->duration;
339
340 // We want the time to be broken down to the very second.
341 // No decimal places for any value, not even for seconds.
342 $this->_sanitize('seconds');
343 if (isset($this->duration['seconds'])) {
344 $this->duration['seconds'] = intval(floor($this->duration['seconds']));
345 }
346
347 // Make sure there's no overflows like 65 seconds per minute.
348 $this->normalize();
349
350 // Assemble the date part.
351 $years = str_pad(intval($this->get_years()), 4, '0', STR_PAD_LEFT);
352
353 if ($this->type() == 'weeks') {
354 $factors = $this->get_conversion_factors();
355 $days_from_weeks = $this->get_weeks() * $factors['days/weeks'];
356 $days_only = str_pad($this->get_days() + $days_from_weeks, 3, '0', STR_PAD_LEFT);
357 $datepart = $years . $date_delimiter . $days_only;
358 }
359 else {
360 $months = str_pad($this->get_months(), 2, '0', STR_PAD_LEFT);
361 $days = str_pad($this->get_days(), 2, '0', STR_PAD_LEFT);
362 $datepart = $years . $date_delimiter . $months . $date_delimiter . $days;
363 }
364 // Assemble the time part.
365 $hours = str_pad(intval($this->get_hours()), 2, '0', STR_PAD_LEFT);
366 $minutes = str_pad(intval($this->get_minutes()), 2, '0', STR_PAD_LEFT);
367 $seconds = str_pad(intval($this->get_seconds()), 2, '0', STR_PAD_LEFT);
368 $timepart = $hours . $time_delimiter . $minutes . $time_delimiter . $seconds;
369
370 // Voilà, an alternative ISO representation (either basic or extended).
371 $datetime = 'P' . $datepart . 'T' . $timepart;
372
373 // Restore the original (non-normalized) duration values.
374 $this->duration = $duration;
375 }
376 else { // $format == 'designators'
377 // Construct the designator strings. Weeks, if they exist, will go in
378 // even if 0, so that we get to keep the 'weeks' format and don't switch
379 // to 'months' if the string is parsed again.
380 $years = ($this->get_years() > 0) ? ($this->duration['years'] . 'Y') : '';
381 $months = ($this->get_months() > 0) ? ($this->duration['months'] . 'M') : '';
382 $weeks = isset($this->duration['weeks']) ? ($this->get_weeks() . 'W') : '';
383 $days = ($this->get_days() > 0) ? ($this->duration['days'] . 'D') : '';
384 $hours = ($this->get_hours() > 0) ? ($this->duration['hours'] . 'H') : '';
385 $minutes = ($this->get_minutes() > 0) ? ($this->duration['minutes'] . 'M') : '';
386 $seconds = ($this->get_seconds() > 0) ? ($this->duration['seconds'] . 'S') : '';
387
388 // Either $months of $weeks is empty anyways, so we can use both.
389 $datepart = $years . $months . $weeks . $days;
390 $timepart = $hours . $minutes . $seconds;
391
392 if (!empty($timepart)) {
393 $timepart = 'T' . $timepart;
394 }
395 if (empty($datepart) && empty($timepart)) {
396 // We need at least one value, let's have zero seconds for that.
397 $timepart = 'T0S';
398 }
399 $datetime = 'P' . $datepart . $timepart;
400 }
401
402 if ($this->is_negative()) {
403 $datetime = '-' . $datetime;
404 }
405 return $datetime;
406 }
407
408
409 /**
410 * Set the date part of this duration to a fixed value, using the
411 * years/months/days format. You may specify numbers greater or equal 0.
412 * As this method involves setting the 'months' value, the 'weeks' value
413 * will be unset if it exists - see setMonths() for the details.
414 * Any invalid parameters will not be applied.
415 */
416 function set_date($years, $months, $days) {
417 $this->_set_value('years', $years, FALSE);
418 $this->_set_value('months', $months, FALSE);
419 $this->_set_value('days', $days);
420 }
421
422 /**
423 * Set the date part of this duration to a fixed value, using the
424 * years/weeks/days format. You may specify numbers greater or equal 0.
425 * As this method involves setting the 'weeks' value, the 'months' value
426 * will be unset if it exists. Any invalid parameters will not be applied.
427 */
428 function set_iso_date($years, $weeks, $days) {
429 $this->_set_value('years', $years, FALSE);
430 $this->_set_value('weeks', $weeks, FALSE);
431 $this->_set_value('days', $days);
432 }
433
434 /**
435 * Set the time part of this duration to a fixed value. You may specify
436 * numbers greater or equal 0. Any invalid parameters will not be applied.
437 */
438 function set_time($hours, $minutes, $seconds = NULL) {
439 $this->_set_value('hours', $hours, FALSE);
440 $this->_set_value('minutes', $minutes, FALSE);
441 $this->_set_value('seconds', $seconds);
442 }
443
444 /**
445 * Set the 'years' value of this duration. You may specify any number
446 * greater or equal 0. In case of an invalid parameter, this method returns
447 * without changing anything.
448 */
449 function set_years($years) {
450 $this->_set_value('years', $years);
451 }
452
453 /**
454 * Set the 'months' value of this duration. You may specify any number
455 * greater or equal 0. Setting the 'months' value automatically unsets
456 * the 'weeks' value if it exists, because those two belong to different
457 * formats and are mutually exclusive. In case of an invalid parameter,
458 * this method returns without changing anything.
459 */
460 function set_months($months) {
461 $this->_set_value('months', $months);
462 }
463
464 /**
465 * Set the 'weeks' value of this duration. You may specify a number
466 * greater or equal 0. Setting the 'weeks' value automatically unsets
467 * the 'months' value if it exists, because those two belong to different
468 * formats and are mutually exclusive. In case of an invalid parameter,
469 * this method returns without changing anything.
470 */
471 function set_weeks($weeks) {
472 $this->_set_value('weeks', $weeks);
473 }
474
475 /**
476 * Set the 'days' value of this duration. You may specify a number
477 * greater or equal 0. In case of an invalid parameter, this method returns
478 * without changing anything.
479 */
480 function set_days($days) {
481 $this->_set_value('days', $days);
482 }
483
484 /**
485 * Set the 'hours' value of this duration. You may specify a number
486 * greater or equal 0. In case of an invalid parameter, this method returns
487 * without changing anything.
488 */
489 function set_hours($hours) {
490 $this->_set_value('hours', $hours);
491 }
492
493 /**
494 * Set the 'minutes' value of this duration. You may specify a number
495 * greater or equal 0. In case of an invalid parameter, this method returns
496 * without changing anything.
497 */
498 function set_minutes($minutes) {
499 $this->_set_value('minutes', $minutes);
500 }
501
502 /**
503 * Set the 'seconds' value of this duration. You may specify a number
504 * greater or equal 0. In case of an invalid parameter, this method returns
505 * without changing anything.
506 */
507 function set_seconds($seconds) {
508 $this->_set_value('seconds', $seconds);
509 }
510
511 /**
512 * Set any value of this duration, like with the set[Metric]() function, only
513 * with a generic method name that takes the metric as parameter. In case
514 * of an invalid parameter, this method returns without changing anything.
515 *
516 * @param $metric
517 * The metric to be set. Possible values: 'seconds', 'minutes', 'hours',
518 * 'days', 'weeks', 'months' and 'years'. In case of 'weeks' and 'months',
519 * please have a look at the API documentation of set_weeks() and
520 * set_months() in order to avoid unexpected behaviour.
521 * @param $value
522 * A number greater or equal 0 that will be set as new value of that metric.
523 */
524 function set_value($metric, $value) {
525 if (!in_array($metric, $this->_allowed_metrics())) {
526 return;
527 }
528 $this->_set_value($metric, $value);
529 }
530
531 function _set_value($metric, $value, $sanitize = TRUE) {
532 if (is_numeric($value) && $value >= 0) {
533 $this->duration[$metric] = $value;
534 }
535
536 // Weeks and months are mutually exclusive, make sure that there's only
537 // one of the two at any time.
538 if ($metric == 'weeks' && isset($this->duration['months'])) {
539 unset($this->duration['months']);
540 }
541 if ($metric == 'months' && isset($this->duration['weeks'])) {
542 unset($this->duration['weeks']);
543 }
544 if ($sanitize) {
545 $this->_sanitize();
546 }
547 }
548
549 function _add_value($metric, $value) {
550 if (!isset($this->duration[$metric])) {
551 $this->duration[$metric] = 0;
552 }
553 $this->duration[$metric] += $value;
554 }
555
556
557 /**
558 * Add this duration to a PHP DateTime object. No return value -
559 * the DateTime object itself will be altered by this function.
560 */
561 function add_to_date($date) {
562 $modifier = '';
563 foreach ($this->duration as $metric => $value) {
564 $modifier = $value . ' ' . $metric;
565 }
566 if (empty($modifier)) {
567 return; // nothing to change
568 }
569 $modifier = ($this->is_negative() ? '-' : '+') . $modifier;
570 date_modify($date, $modifier);
571 }
572
573 /**
574 * Subtract this duration from a PHP DateTime object. No return value -
575 * the DateTime object itself will be altered by this function.
576 */
577 function subtract_from_date($date) {
578 $this->is_negative = !($this->is_negative);
579 $this->add_to_date($date);
580 $this->is_negative = !($this->is_negative);
581 }
582
583
584 /**
585 * Make sure that no part of the duration contains a higher value
586 * than the highest value that is normally used for this metric.
587 * Example: a duration of 65.5 seconds would be transformed into
588 * 1 minute and 5.5 seconds.
589 *
590 * Note that the 'months' and 'years' metrics are likely to cause inaccurate
591 * results because months and years have differences in length depending on
592 * which month or year this applies to. As an approximization, months are
593 * by default calculated by using a conversion factor of 30 days,
594 * and (365 / 7) is used for weeks in a year.
595 *
596 * @param $stop_at_metric
597 * Normally, values are propageted upwards so that large enough "overflows"
598 * get added to the 'years' metric in the end. By setting this to some
599 * smaller metric (say, 'hours'), you might still keep some overflow at
600 * that metric but you also get the guarantee that the metrics above are
601 * not touched by this method. If you want to use this argument, it will
602 * likely be after a set_granularity() or set_largest_metric() call.
603 */
604 function normalize($stop_at_metric = 'years') {
605 $conversion_factors = $this->get_conversion_factors();
606
607 foreach ($this->_metrics('ascending') as $metric => $info) {
608 if ($metric == $stop_at_metric || !isset($info['larger_metric'])) {
609 break;
610 }
611 if ($metric == 'months' && $stop_at_metric == 'weeks') {
612 break;
613 }
614 // No need to check $metric == 'weeks' && $stop_at_metric == 'months',
615 // because 'years' comes after 'weeks' and stops anyways.
616
617 $conversion_key = $metric . '/' . $info['larger_metric'];
618 $conversion_factor = $conversion_factors[$conversion_key];
619
620 if ($this->duration[$metric] > $conversion_factor) {
621 $this->_add_value($info['larger_metric'],
622 intval(floor($this->get_value($metric) / $conversion_factor))
623 );
624 $this->duration[$metric] %= $conversion_factor;
625 }
626 }
627 $this->_sanitize();
628 }
629
630 /**
631 * Set all metrics smaller than @p $smallest_metric to 0 and add their values
632 * to the @p $smallest_metric value using the appropriate conversion factor.
633 * Also, decimal points of larger metrics will be broken down to the metrics
634 * below (but not further below than @p $smallest_metric).
635 *
636 * Note that the 'months' and 'years' metrics are likely to cause inaccurate
637 * results because months and years have differences in length depending on
638 * which month or year this applies to. As an approximization, months are
639 * by default calculated by using a conversion factor of 30 days,
640 * and (365 / 7) is used for weeks in a year.
641 *
642 * @param $smallest_metric
643 * The smallest allowed metric, as mentioned above. Possible values:
644 * 'seconds', 'minutes', 'hours', 'days', 'weeks' (only if type()
645 * returns 'weeks'), 'months' (only if type() returns 'months'),
646 * and 'years'.
647 */
648 function set_smallest_metric($smallest_metric) {
649 $this->_set_smallest_metric(
650 $smallest_metric, $this->_metrics('ascending')
651 );
652 $this->_sanitize($smallest_metric);
653 }
654
655 function _set_smallest_metric($smallest_metric, $metrics) {
656 $conversion_factors = $this->get_conversion_factors();
657
658 foreach ($metrics as $metric => $info) {
659 if ($metric == $smallest_metric || !isset($info['larger_metric'])) {
660 break;
661 }
662 $conversion_key = $metric . '/' . $info['larger_metric'];
663 $conversion_factor = $conversion_factors[$conversion_key];
664
665 $this->_add_value($info['larger_metric'],
666 $this->get_value($metric) / $conversion_factor
667 );
668 if ($metric != 'weeks') { // kept in order to distinguish between 'months' and 'weeks' formats
669 unset($this->duration[$metric]);
670 }
671 }
672 }
673
674 /**
675 * Set all metrics larger than @p $largest_metric to 0 and add their values
676 * to the @p $largest_metric value using the appropriate conversion factor.
677 *
678 * Note that the 'months' and 'years' metrics are likely to cause inaccurate
679 * results because months and years have differences in length depending on
680 * which month or year this applies to. As an approximization, months are
681 * by default calculated by using a conversion factor of 30 days,
682 * and (365 / 7) is used for weeks in a year.
683 *
684 * @param $largest_metric
685 * The largest allowed metric, as mentioned above. Possible values:
686 * 'seconds', 'minutes', 'hours', 'days', 'weeks' (only if type()
687 * returns 'weeks'), 'months' (only if type() returns 'months'),
688 * and 'years'.
689 */
690 function set_largest_metric($largest_metric) {
691 $this->_set_largest_metric(
692 $largest_metric, $this->_metrics('descending')
693 );
694 $this->_sanitize();
695 }
696
697 function _set_largest_metric($largest_metric, $metrics) {
698 $conversion_factors = $this->get_conversion_factors();
699
700 foreach ($metrics as $metric => $info) {
701 if ($metric == $largest_metric || !isset($info['smaller_metric'])) {
702 break;
703 }
704 $conversion_key = $info['smaller_metric'] . '/' . $metric;
705 $conversion_factor = $conversion_factors[$conversion_key];
706
707 $this->_add_value($info['smaller_metric'],
708 $this->get_value($metric) * $conversion_factor
709 );
710 if ($metric != 'weeks') { // kept in order to distinguish between 'months' and 'weeks' formats
711 unset($this->duration[$metric]);
712 }
713 }
714 }
715
716 /**
717 * Set all values and below the @p $smallest_metric and above
718 * the @p $largest_metric to 0, and add those values to the current values
719 * of @p $smallest_metric and @p $largest_metric using the appropriate
720 * conversion factor. Also, decimal points of larger metrics will be
721 * broken down to the metrics below (but not further below than
722 * @p $smallest_metric).
723 *
724 * In other words, this is a convenience function that calls both
725 * set_smallest_metric() and set_largest_metric(). It is the caller's
726 * responsibility to ensure that @p $largest_metric does not specify a
727 * smaller metric than @p $smallest_metric. Same metric for both parameters
728 * is perfectly fine, though.
729 *
730 * Note that the 'months' and 'years' metrics are likely to cause inaccurate
731 * results because months and years have differences in length depending on
732 * which month or year this applies to. As an approximization, months are
733 * by default calculated by using a conversion factor of 30 days,
734 * and (365 / 7) is used for weeks in a year.
735 *
736 * Possible values for both parameters: 'seconds', 'minutes', 'hours',
737 * 'days', 'weeks' (only if type() returns 'weeks'), 'months' (only if type()
738 * returns 'months'), and 'years'.
739 */
740 function set_granularity($smallest_metric, $largest_metric) {
741 // Limit the smallest metric, starting from 'seconds' upwards.
742 $metrics = $this->_metrics('ascending');
743 $this->_set_smallest_metric($smallest_metric, $metrics);
744
745 // Limit the largest metric, starting from 'years' downwards.
746 $metrics = array_reverse($metrics, TRUE);
747 $this->_set_largest_metric($largest_metric, $metrics);
748
749 // Downsize everything so that only $smallest_metric may have a decimal point.
750 $this->_sanitize($smallest_metric);
751 }
752
753
754 /**
755 * Make sure that the duration data is ISO compliant, i.e. no decimal places
756 * are being used for metrics other than the smallest non-zero one.
757 *
758 * @param $smallest_metric
759 * The smallest metric that may contain a decimal place.
760 * If this is not set, the smallest metric of the current duration array
761 * will be used. It is the caller's responsibility to specify a metric
762 * that is as small or smaller than the current smallest metric.
763 */
764 function _sanitize($smallest_metric = NULL) {
765 $metrics_info = $this->_metrics('descending');
766 $conversion_factors = $this->get_conversion_factors();
767
768 if (!isset($smallest_metric)) {
769 $smallest_metric = $this->get_smallest_metric();
770 }
771
772 // If there's a metric with a decimal place and it's not the smallest
773 // metric already, add the decimal place to the next smaller metric.
774 foreach ($metrics_info as $metric => $info) {
775 if ($metric == $smallest_metric) {
776 break;
777 }
778 if (!isset($this->duration[$metric])) {
779 continue; // No need to break down stuff that isn't specified.
780 }
781 if (!$this->_is_whole_number($this->duration[$metric])) {
782 // Retrieve the decimal place and remove it from the current metric.
783 $floor = floor($this->duration[$metric]);
784 $decimal_place = $this->duration[$metric] - $floor;
785 $this->duration[$metric] = intval($floor);
786
787 $conversion_key = $info['smaller_metric'] . '/' . $metric;
788 $conversion_factor = $conversion_factors[$conversion_key];
789
790 // Now add the corresponding value to the smaller metric.
791 $this->_add_value(
792 $info['smaller_metric'], $decimal_place * $conversion_factor
793 );
794 }
795 }
796 }
797
798 /**
799 * Get the smallest metric that is assigned a positive value.
800 * If no part of the duration metrics holds a positive value then
801 * the smallest known metric overall is returned, which is 'seconds'.
802 */
803 function get_smallest_metric() {
804 foreach ($this->_metrics('ascending') as $metric => $info) {
805 if ($this->get_value($metric) > 0) {
806 return $metric;
807 }
808 }
809 return 'seconds';
810 }
811
812 /**
813 * Get the largest metric that is assigned a positive value.
814 * If no part of the duration metrics holds a positive value then
815 * the largest known metric overall is returned, which is 'years'.
816 */
817 function get_largest_metric() {
818 foreach ($this->_metrics('descending') as $metric => $info) {
819 if ($this->get_value($metric) > 0) {
820 return $metric;
821 }
822 }
823 return 'years';
824 }
825
826 /**
827 * Set or reset the metric conversion factors that should be applied for
828 * conversions performed by this duration object. Overriding the original
829 * conversion factors makes it possible to perform calculations for work days
830 * and work weeks (e.g. 8 hours a day, 5 days a week) instead of full 24/7.
831 *
832 * @param $overrides
833 * An array containing new conversion factors to be set. The array doesn't
834 * have to cover all metrics, if some are missing then the existing factors
835 * will still be used.
836 *
837 * Array keys go by the form '{smallermetric}/{largermetric}' (both plural),
838 * and the corresponding values indicate the conversion factor.
839 * Example (the above-mentioned work day/week conversion):
840 * <code>array('hours/days' => 8, 'days/weeks' => 5)</code>.
841 * Note that this only works for adjacent metrics, for example you can't do
842 * something like <code>array('hours/weeks' => 40)</code>.
843 *
844 * Passing the default value NULL resets all factors back to their
845 * original values.
846 */
847 function set_conversion_factors($overrides = NULL) {
848 if (!isset($overrides)) {
849 $overrides = -1;
850 }
851 $this->_conversion_factors($overrides);
852 }
853
854 function get_conversion_factors() {
855 return $this->_conversion_factors();
856 }
857
858 /**
859 * Set/amend, reset or get the conversion factors.
860 */
861 function _conversion_factors($overrides = NULL) {
862 if (is_numeric($overrides)) { // $overrides == -1, coming from the setter
863 unset($this->conversion_factors);
864 }
865 if (!isset($this->conversion_factors)) {
866 $this->conversion_factors = array(
867 'seconds/minutes' => 60,
868 'minutes/hours' => 60,
869 'hours/days' => 24,
870 'days/weeks' => 7,
871 'days/months' => 30, // not accurate in the general case
872 'weeks/years' => (365.0 / 7.0), // not accurate in the general case
873 'months/years' => 12,
874 );
875 }
876 if (is_array($overrides)) {
877 $this->conversion_factors = array_merge(
878 $this->conversion_factors, $overrides
879 );
880 }
881 return $this->conversion_factors;
882 }
883
884 /**
885 * Return all metrics that can be set, regardless of the months/weeks conflict.
886 */
887 function _allowed_metrics() {
888 return array('years', 'months', 'weeks', 'days', 'hours', 'minutes', 'seconds');
889 }
890
891 function _metrics($sort = 'ascending') {
892 if ($sort != 'ascending' && $sort != 'descending') { // bad caller!
893 return array();
894 }
895 $type = $this->type();
896
897 // _metrics() is called so often, let's do a little caching.
898 static $metrics_cache;
899 if (!isset($metrics_cache)) {
900 $metrics_cache = array(); // PHP can't initialize statics with an array directly
901 }
902 if (isset($metrics_cache[$type])) {
903 return $metrics_cache[$type][$sort];
904 }
905
906 // Ok, now the real result array.
907 $metrics = array(
908 'seconds' => array(
909 'larger_metric' => 'minutes',
910 ),
911 'minutes' => array(
912 'smaller_metric' => 'seconds',
913 'larger_metric' => 'hours',
914 ),
915 'hours' => array(
916 'smaller_metric' => 'minutes',
917 'larger_metric' => 'days',
918 ),
919 'days' => array(
920 'smaller_metric' => 'hours',
921 'larger_metric' => $type,
922 ),
923 $type => array( // either 'months' or 'weeks'
924 'smaller_metric' => 'days',
925 'larger_metric' => 'years',
926 ),
927 'years' => array(
928 'smaller_metric' => $type,
929 ),
930 );
931
932 // Fill the cache, buddy!
933 $metrics_cache[$type]['ascending'] = $metrics;
934 $metrics_cache[$type]['descending'] = array_reverse($metrics, TRUE);
935
936 return $metrics_cache[$type][$sort];
937 }
938
939 /**
940 * Straight copy from the php.net comments for is_numeric(), minus
941 * the is_numeric() check itself. Returns TRUE for 2.00000000000,
942 * but will return FALSE for 2.00000000001.
943 */
944 function _is_whole_number($var) {
945 return (intval($var) == floatval($var));
946 }
947 }
948

  ViewVC Help
Powered by ViewVC 1.1.2