root/fDate.php

Revision 587, 11.1 kB (checked in by wbond, 9 months ago)

Added fTimestamp::registerUnformatCallback() to allow for locale-specific date/time/timestamp parsing. The rest of Flourish was updated to use fDate/fTime/fTimestamp instead of strtotime() so that localization affects all date/time/timestamp actions.

LineHide Line Numbers
1 <?php
2 /**
3  * Represents a date as a value object
4  *
5  * @copyright  Copyright (c) 2008-2009 Will Bond
6  * @author     Will Bond [wb] <will@flourishlib.com>
7  * @license    http://flourishlib.com/license
8  *
9  * @package    Flourish
10  * @link       http://flourishlib.com/fDate
11  *
12  * @version    1.0.0b7
13  * @changes    1.0.0b7  Added a call to fTimestamp::callUnformatCallback() in ::__construct() for localization support [wb, 2009-06-01]
14  * @changes    1.0.0b6  Backwards compatibility break - Removed ::getSecondsDifference(), added ::eq(), ::gt(), ::gte(), ::lt(), ::lte() [wb, 2009-03-05]
15  * @changes    1.0.0b5  Updated for new fCore API [wb, 2009-02-16]
16  * @changes    1.0.0b4  Fixed ::__construct() to properly handle the 5.0 to 5.1 change in strtotime() [wb, 2009-01-21]
17  * @changes    1.0.0b3  Added support for CURRENT_TIMESTAMP and CURRENT_DATE SQL keywords [wb, 2009-01-11]
18  * @changes    1.0.0b2  Removed the adjustment amount check from ::adjust() [wb, 2008-12-31]
19  * @changes    1.0.0b   The initial implementation [wb, 2008-02-10]
20  */
21 class fDate
22 {
23     /**
24     * Composes text using fText if loaded
25     *
26     * @param  string  $message    The message to compose
27     * @param  mixed   $component  A string or number to insert into the message
28     * @param  mixed   ...
29     * @return string  The composed and possible translated message
30     */
31     static protected function compose($message)
32     {
33         $args = array_slice(func_get_args(), 1);
34        
35         if (class_exists('fText', FALSE)) {
36             return call_user_func_array(
37                 array('fText', 'compose'),
38                 array($message, $args)
39             );
40         } else {
41             return vsprintf($message, $args);
42         }
43     }
44    
45    
46     /**
47     * A timestamp of the date
48     *
49     * @var integer
50     */
51     private $date;
52    
53    
54     /**
55     * Creates the date to represent, no timezone is allowed since dates don't have timezones
56     *
57     * @throws fValidationException  When `$date` is not a valid date
58     *
59     * @param  fDate|object|string|integer $date  The date to represent, `NULL` is interpreted as today
60     * @return fDate
61     */
62     public function __construct($date=NULL)
63     {
64         if ($date === NULL) {
65             $timestamp = time();
66         } elseif (is_numeric($date) && ctype_digit($date)) {
67             $timestamp = (int) $date;
68         } elseif (is_string($date) && in_array(strtoupper($date), array('CURRENT_TIMESTAMP', 'CURRENT_DATE'))) {
69             $timestamp = time();
70         } else {
71             if (is_object($date) && is_callable(array($date, '__toString'))) {
72                 $date = $date->__toString();   
73             } elseif (is_numeric($date) || is_object($date)) {
74                 $date = (string) $date;   
75             }
76            
77             $date = fTimestamp::callUnformatCallback($date);
78            
79             $timestamp = strtotime(fTimestamp::fixISOWeek($date));
80         }
81        
82         $is_51    = fCore::checkVersion('5.1');
83         $is_valid = ($is_51 && $timestamp !== FALSE) || (!$is_51 && $timestamp !== -1);
84        
85         if (!$is_valid) {
86             throw new fValidationException(
87                 'The date specified, %s, does not appear to be a valid date',
88                 $date
89             );
90         }
91        
92         $this->date = strtotime(date('Y-m-d 00:00:00', $timestamp));
93     }
94    
95    
96     /**
97     * All requests that hit this method should be requests for callbacks
98     *
99     * @internal
100      *
101     * @param  string $method  The method to create a callback for
102     * @return callback  The callback for the method requested
103     */
104     public function __get($method)
105     {
106         return array($this, $method);       
107     }
108    
109    
110     /**
111     * Returns this date in `Y-m-d` format
112     *
113     * @return string  The `Y-m-d` format of this date
114     */
115     public function __toString()
116     {
117         return date('Y-m-d', $this->date);
118     }
119    
120    
121     /**
122     * Changes the date by the adjustment specified, only adjustments of a day or more will be made
123     *
124     * @throws fValidationException  When `$adjustment` is not a relative date measurement
125     *
126     * @param  string $adjustment  The adjustment to make
127     * @return fDate  The adjusted date
128     */
129     public function adjust($adjustment)
130     {
131         $timestamp = strtotime($adjustment, $this->date);
132        
133         if ($timestamp === FALSE || $timestamp === -1) {
134             throw new fValidationException(
135                 'The adjustment specified, %s, does not appear to be a valid relative date measurement',
136                 $adjustment
137             );
138         }
139        
140         return new fDate($timestamp);
141     }
142    
143    
144     /**
145     * If this date is equal to the date passed
146     *
147     * @param  fDate|object|string|integer $other_date  The date to compare with, `NULL` is interpreted as today
148     * @return boolean  If this date is equal to the one passed
149     */
150     public function eq($other_date=NULL)
151     {
152         $other_date = new fDate($other_date);
153         return $this->date == $other_date->date;
154     }
155    
156    
157     /**
158     * Formats the date
159     *
160     * @throws fValidationException  When a non-date formatting character is included in `$format`
161     *
162     * @param  string $format  The [http://php.net/date date()] function compatible formatting string, or a format name from fTimestamp::defineFormat()
163     * @return string  The formatted date
164     */
165     public function format($format)
166     {
167         $format = fTimestamp::translateFormat($format);
168        
169         $restricted_formats = 'aABcegGhHiIOPrsTuUZ';
170         if (preg_match('#(?!\\\\).[' . $restricted_formats . ']#', $format)) {
171             throw new fProgrammerException(
172                 'The formatting string, %1$s, contains one of the following non-date formatting characters: %2$s',
173                 $format,
174                 join(', ', str_split($restricted_formats))
175             );
176         }
177        
178         return fTimestamp::callFormatCallback(date($format, $this->date));
179     }
180    
181    
182     /**
183     * Returns the approximate difference in time, discarding any unit of measure but the least specific.
184     *
185     * The output will read like:
186     *
187     *  - "This date is `{return value}` the provided one" when a date it passed
188     *  - "This date is `{return value}`" when no date is passed and comparing with today
189     *
190     * Examples of output for a date passed might be:
191     *
192     *  - `'2 days after'`
193     *  - `'1 year before'`
194     *  - `'same day'`
195     *
196     * Examples of output for no date passed might be:
197     *
198     *  - `'2 days from now'`
199     *  - `'1 year ago'`
200     *  - `'today'`
201     *
202     * You would never get the following output since it includes more than one unit of time measurement:
203     *
204     *  - `'3 weeks and 1 day'`
205     *  - `'1 year and 2 months'`
206     *
207     * Values that are close to the next largest unit of measure will be rounded up:
208     *
209     *  - `6 days` would be represented as `1 week`, however `5 days` would not
210     *  - `29 days` would be represented as `1 month`, but `21 days` would be shown as `3 weeks`
211     *
212     * @param  fDate|object|string|integer $other_date  The date to create the difference with, `NULL` is interpreted as today
213     * @return string  The fuzzy difference in time between the this date and the one provided
214     */
215     public function getFuzzyDifference($other_date=NULL)
216     {
217         $relative_to_now = FALSE;
218         if ($other_date === NULL) {
219             $relative_to_now = TRUE;
220         }
221         $other_date = new fDate($other_date);
222        
223         $diff = $this->date - $other_date->date;
224        
225         if (abs($diff) < 86400) {
226             if ($relative_to_now) {
227                 return self::compose('today');
228             }
229             return self::compose('same day');
230         }
231        
232         static $break_points = array();
233         if (!$break_points) {
234             $break_points = array(
235                 /* 5 days      */
236                 432000     => array(86400,    self::compose('day'),   self::compose('days')),
237                 /* 3 weeks     */
238                 1814400    => array(604800,   self::compose('week')self::compose('weeks')),
239                 /* 9 months    */
240                 23328000   => array(2592000self::compose('month'), self::compose('months')),
241                 /* largest int */
242                 2147483647 => array(31536000, self::compose('year')self::compose('years'))
243             );
244         }
245        
246         foreach ($break_points as $break_point => $unit_info) {
247             if (abs($diff) > $break_point) { continue; }
248            
249             $unit_diff = round(abs($diff)/$unit_info[0]);
250             $units     = fGrammar::inflectOnQuantity($unit_diff, $unit_info[1], $unit_info[2]);
251             break;
252         }
253        
254         if ($relative_to_now) {
255             if ($diff > 0) {
256                 return self::compose(
257                     '%1$s %2$s from now',
258                     $unit_diff,
259                     $units
260                 );
261             }
262            
263             return self::compose(
264                 '%1$s %2$s ago',
265                 $unit_diff,
266                 $units
267             );
268         }
269        
270         if ($diff > 0) {
271             return self::compose('%1$s %2$s after', $unit_diff, $units);
272         }
273        
274         return self::compose('%1$s %2$s before', $unit_diff, $units);
275     }
276    
277    
278     /**
279     * If this date is greater than the date passed
280     *
281     * @param  fDate|object|string|integer $other_date  The date to compare with, `NULL` is interpreted as today
282     * @return boolean  If this date is greater than the one passed
283     */
284     public function gt($other_date=NULL)
285     {
286         $other_date = new fDate($other_date);
287         return $this->date > $other_date->date;
288     }
289    
290    
291     /**
292     * If this date is greater than or equal to the date passed
293     *
294     * @param  fDate|object|string|integer $other_date  The date to compare with, `NULL` is interpreted as today
295     * @return boolean  If this date is greater than or equal to the one passed
296     */
297     public function gte($other_date=NULL)
298     {
299         $other_date = new fDate($other_date);
300         return $this->date >= $other_date->date;
301     }
302    
303    
304     /**
305     * If this date is less than the date passed
306     *
307     * @param  fDate|object|string|integer $other_date  The date to compare with, `NULL` is interpreted as today
308     * @return boolean  If this date is less than the one passed
309     */
310     public function lt($other_date=NULL)
311     {
312         $other_date = new fDate($other_date);
313         return $this->date < $other_date->date;
314     }
315    
316    
317     /**
318     * If this date is less than or equal to the date passed
319     *
320     * @param  fDate|object|string|integer $other_date  The date to compare with, `NULL` is interpreted as today
321     * @return boolean  If this date is less than or equal to the one passed
322     */
323     public function lte($other_date=NULL)
324     {
325         $other_date = new fDate($other_date);
326         return $this->date <= $other_date->date;
327     }
328    
329    
330     /**
331     * Modifies the current date, creating a new fDate object
332     *
333     * The purpose of this method is to allow for easy creation of a date
334     * based on this date. Below are some examples of formats to
335     * modify the current date:
336     *
337     *  - `'Y-m-01'` to change the date to the first of the month
338     *  - `'Y-m-t'` to change the date to the last of the month
339     *  - `'Y-\W5-N'` to change the date to the 5th week of the year
340     *
341     * @param  string $format  The current date will be formatted with this string, and the output used to create a new object
342     * @return fDate  The new date
343     */
344     public function modify($format)
345     {
346        return new fDate($this->format($format));
347     }
348 }
349  
350  
351  
352 /**
353  * Copyright (c) 2008-2009 Will Bond <will@flourishlib.com>
354  *
355  * Permission is hereby granted, free of charge, to any person obtaining a copy
356  * of this software and associated documentation files (the "Software"), to deal
357  * in the Software without restriction, including without limitation the rights
358  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
359  * copies of the Software, and to permit persons to whom the Software is
360  * furnished to do so, subject to the following conditions:
361  *
362  * The above copyright notice and this permission notice shall be included in
363  * all copies or substantial portions of the Software.
364  *
365  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
366  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
367  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
368  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
369  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
370  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
371  * THE SOFTWARE.
372  */