DateFmt class is an attempt to provide a full-fledged date & time formatting routine which not only has rich formatting features but is also 100% international and reads natural.
It can be used in many cases – from simple «Posted on 12.1.11» which can be handled by
date() or strftime() to complex «Posted 2 hours ago».
And there's even more – for example, you can tell the class to apply «ago-format» to
recent dates and use longer format for older records (via IF).
This also applies to future dates.
And, as mentioned above, everything is 100%-localizable.
DateFmt has no external dependencies except for PHP mbstring module (you can avoid it by delegating «mb_*» functions like mb_substr() to standard substr()).
Download DateFmt from GitHub.
As with standard date() and others date format is specified using a string. However, unlike standard functions DateFmt aims to be easy to remember and read (just like our friend project UverseWiki) so it uses different format strings.
Format string (we'll also call it just «format» sometimes) can also contain special constructions: AGO, AT, AGO-IF and their combinations. Such constructions are written in square brackets, e.g.: AGO[t]IF-FAR[d#my] and have case-sensitive names (ago[s] is a normal string while AGO[s] is a special construct).
The following sample format strings demonstrate how DateFmt is used in some common cases. Your can play with format strings at i-Tools.org.
This wiki document uses {{DateFmt}} action for UverseWiki.
Now is D__, AT[d# of M__ y##] (h##ms). | now | Now is Thursday, 15 of July 2011 (08:01:55 PM). |
(ru) Сегодня D__, AT[d# M__ y##] (h##ms). | now | Сегодня Пятница, 15 Июля 2011 (20:01:55). |
|
||
Last commit was AGO[d.h]. | -d*40 - h*14 - 12 | Last commit was 40 days 14 hours ago. |
(ru) Last commit was AGO[d.h]. | -d*40 - h*14 - 12 | Last commit was 40 дней 14 часов назад. |
|
||
It was on H#:m## A.M.. | now | It was on 20:01 PM. |
(ru) Это было в H#:m## A.M.. | now | Это было в 20:01 вечера. |
|
||
This entry was posted AGO[*]AT D__, d# M__ y##. | -m*50 - 3 | This entry was posted 50 minutes ago at Thursday, 15 July 2011. |
This document was created on d#my. | @2011-01-12 | This document was created on 12/01/2011. |
(ru) This document was created on d#my. | @2011-01-12 | This document was created on 12.01.2011. |
|
||
A diary post saying //AT[d# m__ y##]// in its top right corner. | @2012-12-12 | A diary post saying 12 december 2012 in its top right corner. |
(ru) Записка с надписью //AT[d# m__ y##]// в правом верхнем углу. | @2012-12-12 | Записка с надписью 12 декабря 2012 в правом верхнем углу. |
(ru) Записка с надписью //d# m__ y##// в правом верхнем углу. | @2012-12-12 | Записка с надписью 12 декабрь 2012 в правом верхнем углу. |
|
||
This reply was posted AGO[s-d]IF-FAR[on d#my]AT D__. | +h*3 | This reply was posted after 3 hours at Thursday. |
This reply was posted AGO[s-d]IF-FAR[on d#my]AT D__. | +d*40 | This reply was posted on 24/08/2011 at Wednesday. |
|
||
Posted at d##-M_-y# h##m (AGO[h-y]_ since last post... | +h*0.5 | Posted at 15-Jul-11 08:31 PM (half an hour since last post… |
...and AGO[*]_ before next reply). | -m*5 | …and 5 minutes before next reply). |
(ru) Добавлено d##-M_-y# h##m (через AGO[h-y]_ после предыдущего сообщения... | +h*0.5 | Добавлено 15-Июл-11 20:31 (через полчаса после предыдущего сообщения… |
(ru) ...и за AGO[*]_ перед следующим). | -m*5 | …и за 5 минут перед следующим). |
Syntax described here is used to format strings like «1 November 2010» or «5.03.2010» and is similar to PHP's date() function. However, format strings are different.
Generic format for a format string is a character followed by 1 or 2 hashes (#). Character represents a date/time element to output (days, hours, etc.) while hashes – their size or padding. For example, d# outputs current day without leading zero; d## outputs the same date but if it's 1-9 it'll be padded with one zero.
List of formats (case-insensitive unless mentioned; 9th January 2011 08h:02m:05s is used as example datetime):
You can insert AM/PM (Ante meridiem and Post meridiem) markers using lower-case a.m. or upper-case A.M.. Note that AM/PM notation isn't used in some countries, though, so if you can use shortcut formats like h##m (see below) – they'll use language data for this.
Note: languages that don't normally use AM/PM might have different names for them that have similar meaning – for example, if you want to say something like «2:00 in the evening» write it as h#:m## A.M. and it'll be shown as «6:51 PM» in English and «6:51 вечера» in Russian (which is translated exactly as «in the evening»).
There are also 4 shortcut formats for full date and time that will respect
national formats (e.g. 1/24/2010 for Americans and 24.01.2010 for Russians) along
with usage of 12- or 24-hour time format.
These shortcut formats are:
You can also format date & time into names: 01 → January or Jan and so on.
There are 2 forms for most names – full (January) and short (Jan).
Name formats are defined similar to other date & time formatting – a character
followed by 1 or 2 underscores (_).
These formats are case-sensitive – if you use upper-case letter (e.g. M__) month name will be output capitalized; on the contrary, m__ will output «january».
Both m_ (M_) and m__ (M__) have aliases «mo_» (Mo_, mo__, Mo__) – if you would like to follow the style of the mo# date format.
The following 2 might look unexpected yet they correspond to our 100%-localizable paradigm (unlike the above they're case-insensitive):
This comes in handy when you need to reflect the distance between 2 dates. For example,
on forums a post can be added «after 5 hours» since the last post. Or a to-do item's
deadline can expire «in 12 days».
Thus we see that it's possible to format both past & future periods.
To turn this mode on you need to use AGO[] construct, for example: AGO[t]. AGO[] can also be combined with AT which is described here.
Note that there's also exact AGO[] mode which sligtly differs from the one described here.
This mode works as follows: you specify a set of periods (1 period = 1 character) that the given distance will be checked against; if at least one specified range could represent it without fraction (e.g. 5 minutes can be represented both as 300 seconds and 0.08 hours) then it's used, otherwise nearest smallest specified period is taken and the distance is then displayed as a fraction with 1 trailing digit (%1.1f format).
If more distances fall in between several ranges the result is rounded to the smallest
range (e.g. AGO[smh] results in 5 minutes rather than 0 hours).
For this reason you don't have to specify consecutive ranges (e.g. sec, min, hour)
– if you need you can skip some of them (e.g. sec, hour) and even if a distance
falls perfectly into the skipped range (e.g. 30 minutes) the nearest specified
range will be used (here it'll be 1800 seconds).
Sometimes you'd like to display an exact interval in which something has taken place –
«2 days 45 minutes 3 seconds ago», or its shortened form: «2d 45min 3s ago».
You can do it using this construct: AGO[d.i.s].
Unlike normal AGO[] form which contains no dots this one displays all ranges regardless if one of them has already been matched. For example:
Ranges are the same as for AGO[] – they're also case-insensitive; the order in which they're defined matters: AGO[y.h.w] outputs «1 year 2 hours 4 weeks ago» while AGO[w.y.h] outputs «4 weeks 1 year 2 hours ago».
There are differences between normal AGO[] mode and this one:
You can combine any construct that can be used with normal AGO[] – AGO-SHORT, IF[], AT, AGO[]_ and so on – with this exact AGO[]:
As mentioned in section on AGO[], fractions are never output unless there are
no ranges that could
contain given distance (AGO[h] against 5 minutes is 0.08 hours – minute
range «m» wasn't specified).
But if there's at least one matching range it'll be used – no matter how large number
shows up: AGO[sm] against 10 days will yield 14400 minutes.
However, for more natural feeling this fraction rule has 2 exceptions (both work only if short mode (AGO-SHORT[]) wasn't enabled.
First exception is for numbers between 1.45-1.99 – they will be output as a string
(if language supports it) – in English this would sound rather long («one minute and a half»)
thus it's not used but in some others (e.g. Russian) it'll be displayed as «полторы минуты»
instead of «1 минута».
This also gives more accurate feeling of periods while avoiding fractions – «1» can
refer to 1, 1.2, 1.6, etc. while «one and a half» means at least 1.5 and above –
twice as accurate than just «1».
Second exception is for numbers between 0.45 and 0.99 – the idea is the same as above. Example: 0.8 hours → half an hour ago.
The following ranges are defined (case-insensitive):
There are also these special ranges (can be used in combination with regular ones):
If there are any duplicated ranges they are discarded; if there are wrong characters that don't correspond to any range an exception is thrown. An error will also occur if you're using - but its start or end range was wrong.
Order in which you specify ranges doesn't matter (unless you're using a dash): AGO[ywm] is identical to AGO[wmy] and any other combination of y, w and m.
You can also specify a consecutive set of ranges using a dash (–) or several dashes:
Note: you can't specify b as a start or end range for -, e.g. this won't work: AGO[b-y].
By default AGO[] will say «…ago» or «after...». However, in some situations
you'd like to place your own word (thus binding format string to your language but
perhaps making it sound more natural).
This can be achieved by putting an underscore (_) after AGO[] or its
IF[]. Compare the following examples:
Sometimes you'd like to make AGO output result in more concise form.
For example, AGO-SHORT[d] against 2 days will output «2d ago» instead of
«2 days ago».
The same goes for the b range albeit English words for «today» and
«yesterday» («tomorrow») are already short enough and are not shortened further – but
«day after today» becomes just «day after».
How short the words will be is language-dependent – but generally in languages using letters (i.e. not Japanese, etc.) a short word is adviced to have 7-8 characters and not more than 10 characters – contractions many be used when necessary.
Note: there's no «short mode» for regular date & time formatting as there already are pairs of full/short format strings: M_ and M__ – see this section for details.
There are certain situations when it's better to output full date instead of something like «21 days ago» (making user calculate the exact date). This can be done using IF[] construct.
This construction can only be used along with AGO[] and has 2 forms:
How this works? Really simple: the number you specify («n») is compared against the distance and if it's larger than that AGO[] block is not used – instead, the format passed for IF[] is displayed.
Note that distance is taken based on ranges that you've specified for AGO[] – compare these examples:
What kind of format does IF[] accept? It's the same date & time formatting described above.
FAR shortcut lets you specify numbers relative to the longest range in AGO[].
For example, FAR number for AGO[sh] is 24 – since a day has 24 hours.
Complete list of range → FAR numbers:
IF can be used in following combinations:
In every case IF can be either IF-FAR or IF>n; also in every case AT can be appended: ...IF[...]AT.
This construct (actually they're two) is meant exactly for situations like «2 days ago at 0:01». However, ithis phrase is only natural to English – but no matter how localizable a project is if it says here and there something like «два дня назад at 0:01» it has chances to look worse than it would with no localization at all.
It's a bit hard to explain the concept based on English grammar rules (maybe because I'm
not a native English speaker or a linguist) but it makes sense in many other languages
so I'll try to.
Basically, there are 2 kind of dates:
Using these 2 forms we can untie this knot.
The difference between both forms is that the first doesn't output «at» – it only changes word forms in languages that need this.
AT can be combined with AGO[], AGO-IF[] or just used on its own:
IF[]:
All other combinations are also possible – for example, you can combine everything into one: AGO-SHORT-AT[...]IF[...]AT.
Once again note that placing AT on both sides is redundant – trailing AT already implies leading.
DateFmt tries to be as much localizable as possible, including conforming to national standards when possible. Localization is carried out by DateFmt::$languages property which is assigned default language strings at the end of date.php file.
There are many languages which change word ending based on number it's connected to. In English this is just an addition of «s» to numbers other than 1: 0 weeks, 1 week, 2 weeks, etc. Other languages have more complicated rules, e.g. in Russian there are 4 different word endings for counters – additionally they are repeated after each 100th number.
PHP'number rolls'
string of the localization array contains true/false value:
PHPtrue
PHPfalse
Line endings can only be specified for certain strings. For example, let's take
«AGO s»: PHParray(' second', 's', '', 's', 's')
PHPtrue
or for any number above 4 otherwise.
For example: PHParray('stem-', '9', '1', '2', '3')
If you specify a string instead of array for a line which supports word endings it'll be treated as constant for any number regardless of its value.
List of strings supporting word endings:
PHP"AGO s"
– PHP"AGO y"
(i.e. s, m, h, d, s, w, y).PHP"AGO s-at"
– PHP"AGO y-at"
.PHP"AGO short s"
– PHP"AGO short y"
.PHP"AGO short s-at"
– PHP"AGO short y-at"
.See also AT-form translation below.
There's a possibility to specify certain national formats and other info for languages using strings. The list of string name → description:
An attempt to describe what AT-form was made in this secontion. For English this form is unused.
Certain language strings can have AT-form in addition to regular. If such a string had no AT-form specified regular one will be used instead.
List of strings that can have AT-form:
PHP"AGO s-at"
– PHP"AGO y-at"
(i.e. s, m, h, d, s, w, y).PHP"AGO short s-at"
– PHP"AGO short y-at"
.As described in Natural fractions sometimes DateFmt might want to output some fractions as words instead. This includes the following strings:
PHP"half s"
– PHP"half y"
(i.e. s, m, h, d, s, w, y) – for fractions like 0.5 sec, 0.9 years.PHP"1.5 s"
– PHP"1.5 y"
– for fractions like 1.5 days, 1.9 min.If no string is supplied for a particular fraction it'll always be output in numerical form (e.g. English has no strings for 1.5 fractions).
To format a timestamp you need to use DateFmt->FormatAs() method or you can call static DateFmt::Format() which will construct the object and set some settings.
Make sure that internal encoding of mbstring is set to UTF-8 before calling FormatAs(). This is not necessary for static Format() as it does so automatically (and restores original encoding before returning).
$your_timestamp = 158399691; // => Wednesday, 08/01/1975
$formatted = DateFmt::Format('D__, d##my', $your_timestamp, 'ru');
$date = new DateFmt( $entry['timestamp'] );
$date->LoadLanguage('br');
echo $date->FormatAs('Now: d#my h##m.'); // => Now: 08/01/1975 10:48.
// null $date equals to time(); null $language - to 'en'.
static function Format($str, $date = null, $language = null)
// the same but as an instance method.
function FormatAs($str)
// if array, will be used as is; if string it's considered an ISO-639-2 lang code (e.g. 'en').
function LoadLanguage($iso2OrArray)
// returns an array containing the result of running self tests defined in self::$selfTests.
static function RunSelfTests()
public $date
public $now = time()
static public $selfTests // an array of self-tests to be executed by RunSelfTests().
By setting $now you can format dates (e.g. using AGO[]) as future:
$date = new DateFmt;
$date->date = $lastPost['timestamp'];
$date->now = $currentPost['timestamp'];
echo $date->FormatAs('Posted AGO[*]_ before the last post in this thread.');
// => Posted 2 days before the last post in this thread.
Most of the time you wouldn't need to access them as they're used internally. However,
they are standalone and might prove useful in some cases.
Fields not listed here but present in the class definition are considered private and
might be changed later.
PHPstatic $languages
PHParray('string name' => 'localized string', ...)
. This array is populated at the end of date.php.PHP$strings
PHPFmtStr($name, $args)
PHPFmtNum($number, $langName)
PHPstatic FmtNumUsing($stem, $inflections, $numberRolls, $number)
PHPstatic FixFloat($float)
PHPfloor((0.1 + 0.7) * 10)
will result in 7.9999... float that upon removing fractional part will become by 1 less than it actually should be. This function will explicitly round up numbers that are ≥ 0.9999.PHPFormatNormal($format, $wordForm = '')
PHPFormatDate($dayLeadZero = false, $withYear = true)
PHPFormatTime($hoursLeadZero = false, $withSeconds = false)
PHPFormatAGO($format, $options = array())
All exceptions that DateFmt might throw are derived from its DateError class (which inherits from standard Exception).
«Expected» exceptions usually occuring because of user actions have their own classes; others use base DateError class with custom error messages.
Errors occuring when formatting datetime:
This exception is thrown if last call to a preg_* function has failed and
preg_last_error() didn't return 0. It has PHP$pcreErrorCode
instance property and
ThrowIfPcreFailed() static method.
This exception is thrown when an attempt to load a language was made but there were
no localization for it (no entry in PHPDateFmt::$languages
). Base language that is
always present is PHP"en"
(English).
It has PHP$iso2
instance property containing two-character
ISO-639-2 language code (e.g. en, ru, jp).
This exception is thrown from DateFmt->LoadLanguage() method, which is also called by __construct().
This exception is thrown in case of parsing errors – for example, [...]IF[...]AT will cause this error as it must be either AGO[...]IF[...]AT, AGO[...]IF or just [...]AT.