DateFmt
  1. 1. Overview
  2. 2. Common usage examples
  3. 3. Date & time formatting
    1. 3.1. Names
    2. 3.2. [AGO] Relative time – before & later
    3. 3.3. Exact AGO
      1. 3.3.1. Natural fractions
    4. 3.4. Ranges
    5. 3.5. Suppressing «ago» or «after» words
    6. 3.6. AGO-SHORT
    7. 3.7. IF-FAR and IF>n
    8. 3.8. [AT] Indication of time
      1. 3.8.1. Combinations
  4. 4. Localization coverage
    1. 4.1. Numbers
    2. 4.2. National standards
    3. 4.3. AT-form
    4. 4.4. Word fractions
  5. 5. Developer's reference
    1. 5.1. Formatting example
    2. 5.2. Future AGO[] via $now
    3. 5.3. Other methods & properties
    4. 5.4. Exceptions
      1. 5.4.1. Locations where exceptions are thrown
      2. 5.4.2. EDateLastPCRE
      3. 5.4.3. EDateLanguage
      4. 5.4.4. EDateParse

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.

Overview

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).

Common usage examples

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 минут перед следующим).

Date & time formatting

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):

d# or d##
day (9 or 09);
mo# or mo##
month (1 or 01) – note that it's «mo» instead of «m» which is used to specify minutes;
w# or w##
week number (2 or 02);
y# or y##
year (11 or 2011) – note that for year a single hash doesn't mean «pad to 1 digit» but «pad to 2 digits» as it makes no sense;
s# or s##
seconds (5 or 05);
m# or m##
minutes (2 or 02) – don't confuse with month which is «mo»;
h# or h##
hours in 12-hour format (8 or 08) – character case makes it different from «H»;
H# or H##
hours in 24-hour format (8 or 08).

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 meaningfor 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:

d#my or d##my
9.01.2011 or 09.01.2011;
d#m or d##m
without year: 9.01 or 09.01;
h#ms or h##ms
2:08:50 or 02:08:50 – here unlike with H## character case of h doesn't mattern – whether to use 12-hour format or not is language-dependent anyway;
h#m or h##m
without seconds: 2:08 or 02:08.

Names

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».

The list of name formats:

d_ or D_
short day name (mon or Mon);
d__ or D__
full day name (monday or Monday);
m_ or M_
short month name (jan or Jan);
m__ or M__
full month name (january or 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):

y_
abbreviature of word «year» (y.);
y__
full word «year» (year).

[AGO] Relative time – before & later

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).

Exact AGO

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:

  1. AGO[dms] when matched against 2 days 45 minutes ago will yield just «2 days ago»;
  2. AGO[d.m.s] matched against the same date will result in «2 days 45 minutes 0 seconds ago».

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:

  1. You cannot use «m» as a range as it's ambiguous (month & minute) – use «o» and «i» instead.
  2. Dash (–) cannot be used to specify consequent ranges.
  3. Range shortcuts (t * b) cannot be used.
  4. Natural fractions are not used – fractions are never output.
  5. It's not possible to have a single range (e.g. AGO[d]) – it's considered normal AGO[].

You can combine any construct that can be used with normal AGO[]AGO-SHORT, IF[], AT, AGO[]_ and so on – with this exact AGO[]:

Natural fractions

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 hourshalf an hour ago.

Ranges

The following ranges are defined (case-insensitive):

  1. s (seconds);
  2. m (minutes) or i – it's the same;
  3. h (hours);
  4. d (days);
  5. w (weeks);
  6. o (months) – m can also be used to specify months – for this it must have m somewhere before it («smhdm»), be prefixed with either of d, b, w («dm») or be followed by y («my»).
  7. y (years).

There are also these special ranges (can be used in combination with regular ones):

t
implies time ranges: smh;
*
implies all defined ranges:
b
a special range that works exactly as d unless distance between 2 dates is 3 days or shorter – in this case instead of robot-like «2 days ago» message «yesterday» will be output. It also works with future dates: after 0 daystomorrow, after 2 daysday after tomorrow.

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].

Suppressing «ago» or «after» words

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:

Posted at d#my (AGO[*] since last message)
will result in something like «…(2 hours ago since last message)».
Posted at d#my (AGO[*]_ since last message)
an underscore after brackets makes it sound like this: «…(2 hours since last message)».

AGO-SHORT

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.

IF-FAR and IF>n

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:

  1. IF>n[...] where «n» is any number except zero: IF>1, IF>15 and so on;
  2. IF-FAR[...] – a shortcut which expands into IF>n, see below.

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:

  1. AGO[m]IF>6[d#m], when 5 days have passed. While 5 days is closer (less) than «6 days», «6 days» is still closer than 7200 minutes (5 * 24 * 60) – thus IF's format string is used instead of AGO's.
  2. AGO[md]IF>6[d#m], again when 5 days have passed. This time there's a d range added so distance calculated isn't 7200 minutes but is 5 days – it's less than 6 (IF>6) and for this reason AGO's expression is used.

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:

s
60
m
60
h
24
d (b)
30
w
4
m
12
y
Cannot be used – an exception is thrown.

IF can be used in following combinations:

  1. AGO[...]IF[...]
  2. AGO-AT[...]IF[...]
  3. AGO-SHORT[...]IF[...]
  4. AGO-SHORT-AT[...]IF[...]

In every case IF can be either IF-FAR or IF>n; also in every case AT can be appended: ...IF[...]AT.

[AT] Indication of time

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:

  1. Just a date in the past with no connection to anything – «12 February».
  2. A date which is somehow connected to something else«Today is 12 February», «Posted on 12 February».

Using these 2 forms we can untie this knot.

  1. Posted at AT[D__, d# M__##]«Posted at Wednesday, 12 February» («Posted at Среду, 12 Февраля»).
  2. [d#my]AT h#m«12/01/2011 at 7:07 PM». Note that there's no article «at» specified in format string so it'll work in any other language without modifications, e.g. in Russian: «12.01.2011 в 19:07». Moreover, even national date/time format is retained (d#my and h#m).

The difference between both forms is that the first doesn't output «at» – it only changes word forms in languages that need this.

Combinations

AT can be combined with AGO[], AGO-IF[] or just used on its own:

  1. AT[...]
  2. AT[...]AT
  3. [...]AT – implies AT in front thus is the same as the previous variant.

AGO[]:

  1. AGO[...]AT
  2. AGO-AT[...]AT
  3. AGO-AT[...]

IF[]:

  1. AGO-AT[...]IF[...]
  2. AGO[...]IF[...]AT
  3. AGO-AT[...]IF[...]AT

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.

Localization coverage

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.

Numbers

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
Means that word endings are repeated after each 100th number; this is not the case in English: 1 week, 2 weeks and 101 weeks while being so in Russian: 1 неделя, 2 недели and 101 неделя.
PHPfalse
Uses 4 word endings supplied no matter how large the number is – as in English.

Line endings can only be specified for certain strings. For example, let's take «AGO s»: PHParray(' second''s''''s''s')

  1. word stem (" second" – note leading space, without it number would be glued: «1second»);
  2. ending for 0 («s»);
  3. ending for 1 ("" – empty);
  4. ending for numbers 2-4 («s»);
  5. ending for numbers 5-20 if «number rolls» is PHPtrue or for any number above 4 otherwise.

For example: PHParray('stem-''9''1''2''3')

  1. For 0: stem-0
  2. For 1: stem-1
  3. For 2, 3, 4: stem-2
  4. For others: stem-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:

  1. PHP"AGO s"PHP"AGO y" (i.e. s, m, h, d, s, w, y).
  2. PHP"AGO s-at"PHP"AGO y-at".
  3. PHP"AGO short s"PHP"AGO short y".
  4. PHP"AGO short s-at"PHP"AGO short y-at".

See also AT-form translation below.

National standards

There's a possibility to specify certain national formats and other info for languages using strings. The list of string name → description:

12 hour time
Boolean value, true if language uses 12-hour notation by default and false – if 24-hour (true in English).
AM
Text string for Ante Meridiem or a word(s) of similar meaning if language doesn't use or have AM/PM notation, e.g. «in the morning» («AM» in English);
PM
Text string for Post Meridiem («PM» in English);
float delim
A character used to separate integer and fractional parts of a fraction (period (.) in English);
date without year
Format of full date but without year («d/m» in English);
date with year
Format of full date but with year («d/m/y» in English);
time without seconds
Format for hours and minutes where percent sign (%) is replaced by AM or PM according to date being formatted («h:m %» in English);
time with seconds
Format for hours, minutes and seconds («h:m:s %» in English);

AT-form

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:

  1. PHP"AGO s-at"PHP"AGO y-at" (i.e. s, m, h, d, s, w, y).
  2. PHP"AGO short s-at"PHP"AGO short y-at".

Word fractions

As described in Natural fractions sometimes DateFmt might want to output some fractions as words instead. This includes the following strings:

  1. PHP"half s"PHP"half y" (i.e. s, m, h, d, s, w, y) – for fractions like 0.5 sec, 0.9 years.
  2. 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).

Developer's reference

Formatting example

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).

Static example:

PHP
$your_timestamp 158399691;                // => Wednesday, 08/01/1975
$formatted DateFmt::Format('D__, d##my'$your_timestamp'ru');

Instance example:

PHP
$date = new DateFmt$entry['timestamp'] );
$date->LoadLanguage('br');
echo 
$date->FormatAs('Now: d#my h##m.');    // => Now: 08/01/1975 10:48.

Useful methods of DateFmt:

PHP
// 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()

Useful properties:

PHP
public $date
public $now time()
static public 
$selfTests   // an array of self-tests to be executed by RunSelfTests().

Future AGO[] via $now

By setting $now you can format dates (e.g. using AGO[]) as future:

PHP
$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.

Other methods & properties

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.

Properties:

PHPstatic $languages
An array of language strings; each member is an array of form PHParray('string name' => 'localized string', ...). This array is populated at the end of date.php.
PHP$strings
An array of texts for currently loaded language. It is a copy of corresponding DateFmt::$languags[] element unless LoadLanguage() was directly passed an array of strings.

Methods:

PHPFmtStr($name$args)
Takes a string from $strings, replaces $args in it using strtr() and returns the result.
PHPFmtNum($number$langName)
Formats an integer according to the language string $langName which can be an array with word endings or a string. See also section on number localization.
PHPstatic FmtNumUsing($stem$inflections$numberRolls$number)
Standalone; formats a number according to rules described here.
PHPstatic FixFloat($float)
Due to PHP's floating point precision math some operations like 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 '')
Formats a string that does not contain any constructions like AGO[] and ATi.e. they are not parsed even if present. Handles basic date & time formatting. Currently supported word forms are "" (default) and "at".
PHPFormatDate($dayLeadZero false$withYear true)
Handles d#my, d##my, d#m, d##m of basic date formatting
PHPFormatTime($hoursLeadZero false$withSeconds false)
Handles h#ms, h##ms, h#m, h##m of basic time formatting.
PHPFormatAGO($format$options = array())
Handles all forms of AGO[] including its exact form, IF and others.

Exceptions

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.

Locations where exceptions are thrown

By methods of DateFmt:

__constuct
Calls LoadLanguage() which might throw EDateLanguage.
LoadLanguage
Throws EDateLanguage.
FormatAs
Throws EDateLastPCRE.

Errors occuring when formatting datetime:

ParsePart
Throws EDateParse in case of [...]IF[...]AT match.
FormatAGO and AGO[]-related methods
Throw multiple EDateParse:

EDateLastPCRE

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.

EDateLanguage

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().

EDateParse

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.