/// ----------------------------------------------------------------------------
/// @file   util_i_strftime.nss
/// @author Michael A. Sinclair (Squatting Monk) <squattingmonk@gmail.com>
/// @brief  Functions for formatting times.
/// ----------------------------------------------------------------------------
/// @details This file contains an implementation of C's strftime() in nwscript.
///
/// # Formatting
///
/// You can format a Time using the `strftime()` function. This function takes
/// a Time as the first parameter (`t`) and a *format specification string*
/// (`sFormat`) as the second parameter. The format specification string may
/// contain special character sequences called *conversion specifications*, each
/// of which is introduced by the `%` character and terminated by some other
/// character known as a *conversion specifier character*. All other character
/// sequences are *ordinary character sequences*.
///
/// The characters of ordinary character sequences are copied verbatim from
/// `sFormat` to the returned value. However, the characters of conversion
/// specifications are replaced as shown in the list below. Some sequences may
/// have their output customized using a *locale*, which can be passed using the
/// third parameter of `strftime()` (`sLocale`).
///
/// Several aliases for `strftime()` exist. `FormatTime()`, `FormatDate()`, and
/// `FormatDateTime()` each take a calendar Time and will default to formatting
/// to a locale-specific representation of the time, date, or date and time
/// respectively. `FormatDuration()` takes a duration Time and defaults to
/// showing an ISO 8601 formatted datetime with a sign character before it.
///
/// ## Conversion Specifiers
/// - `%a`: The abbreviated name of the weekday according to the current locale.
///         The specific names used in the current locale can be set using the
///         key `LOCALE_DAYS_ABBR`. If no abbreviated names are available in the
///         locale, will fall back to the full day name.
/// - `%A`: The full name of the weekday according to the current locale.
///         The specific names used in the current locale can be set using the
///         key `LOCALE_DAYS`.
/// - `%b`: The abbreviated name of the month according to the current locale.
///         The specific names used in the current locale can be set using the
///         key `LOCALE_MONTHS_ABBR`. If no abbreviated names are available in
///         the locale, will fall back to the full month name.
/// - `%B`: The full name of the month according to the current locale. The
///         specific names used in the current locale can be set using the key
///         `LOCALE_MONTHS`.
/// - `%c`: The preferred date and time representation for the current locale.
///         The specific format used in the current locale can be set using the
///         key `LOCALE_DATETIME_FORMAT` for the `%c` conversion specification
///         and `ERA_DATETIME_FORMAT` for the `%Ec` conversion specification.
///         With the default settings, this is equivalent to `%Y-%m-%d
///         %H:%M:%S:%f`. This is the default value of `sFormat` for
///         `FormatDateTime()`.
/// - `%C`: The century number (year / 100) as a 2-or-3-digit integer (00..320).
///         (The `%EC` conversion specification corresponds to the name of the
///         era, which can be set using the era key `ERA_NAME`.)
/// - `%d`: The day of the month as a 2-digit decimal number (01..28).
/// - `%D`: Equivalent to `%m/%d/%y`, the standard US time format. Note that
///         this may be ambiguous and confusing for non-Americans.
/// - `%e`: The day of the month as a decimal number, but a leading zero is
///         replaced by a space. Equivalent to `%_d`.
/// - `%E`: Modifier: use alternative "era-based" format (see below).
/// - `%f`: The millisecond as a 3-digit decimal number (000..999).
/// - `%F`: Equivalent to `%Y-%m-%d`, the ISO 8601 date format.
/// - `%H`: The hour (24-hour clock) as a 2-digit decimal number (00..23).
/// - `%I`: The hour (12-hour clock) as a 2-digit decimal number (01..12).
/// - `%j`: The day of the year as a 3-digit decimal number (000..336).
/// - `%k`: The hour (24-hour clock) as a decimal number (0..23). Single digits
///         are preceded by a space. Equivalent to `%_H`.
/// - `%l`: The hour (12-hour clock) as a decimal number (1..12). Single digits
///         are preceded by a space. Equivalent to `%_I`.
/// - `%m`: The month as a 2-digit decimal number (01..12).
/// - `%M`: The minute as a 2-digit decimal number (00..59, depending on
///         `t.MinsPerHour`).
/// - `%O`: Modifier: use ordinal numbers (1st, 2nd, etc.) (see below).
/// - `%p`: Either "AM" or "PM" according to the given Time, or the
///         corresponding values from the locale. The specific word used can be
///         set for the current locale using the key `LOCALE_AMPM`.
/// - `%P`: Like `%p`, but lowercase. Yes, it's silly that it's not the other
///         way around.
/// - `%r`: The preferred AM/PM time representation for the current locale. The
///         specific format used in the current locale can be set using the key
///         `LOCALE_AMPM_FORMAT`. With the default settings, this is equivalent
///         to `%I:%M:%S %p`.
/// - `%R`: The time in 24-hour notation. Equivalent to `%H:%M`. For a version
///         including seconds, see `%T`.
/// - `%S`: The second as a 2-digit decimal number (00..59).
/// - `%T`: The time in 24-hour notation. Equivalent to `%H:%M:%S`. For a
///         version without seconds, see `%R`.
/// - `%u`: The day of the  week as a 1-indexed decimal (1..7).
/// - `%w`: The day of the week as a 0-indexed decimal (0..6).
/// - `%x`: The preferred date representation for the current locale without the
///         time. The specific format used in the current locale can be set
///         using the key `LOCALE_TIME_FORMAT` for the `%x` conversion
///         specification and `ERA_TIME_FORMAT` for the `%Ex` conversion
///         specification. With the default settings, this is equivalent to
///         `%Y-%m-%d`. This is the default value of `sFormat` for
///         `FomatDate()`.
/// - `%X`: The preferred time representation for the current locale without the
///         date. The specific format used in the current locale can be set
///         using the key `LOCALE_DATE_FORMAT` for the `%X` conversion
///         specification and `ERA_DATE_FORMAT` for the `%EX` conversion
///         specification. With the default settings, this is equivalent to
///         `%H:%M:%S`. This is the default value of `sFormat` for
///         `FormatTime()`.
/// - `%y`: The year as a 2-digit decimal number without the century (00..99).
///         (The `%Ey` conversion specification corresponds to the year since
///         the beginning of the era denoted by the `%EC` conversion
///         specification.)
/// - `%Y`: The year as a decimal number including the century (0000..32000).
///         (The `%EY` conversion specification corresponds to era key
///         `ERA_FORMAT`; with the default era settings, this is equivalent to
///         `%Ey %EC`.)
/// - `%%`: A literal `%` character.
///
/// ## Modifier Characters
/// Some conversion specifications can be modified by preceding the conversion
/// specifier character by the `E` or `O` *modifier* to indicate that an
/// alternative format should be used. If the alternative format does not exist
/// for the locale, the behavior will be as if the unmodified conversion
/// specification were used.
///
/// The `E` modifier signifies using an alternative era-based representation.
/// The following are valid: `%Ec`, `%EC`, `%Ex`, `%EX`, `%Ey`, and `%EY`.
///
/// The `O` modifier signifies representing numbers in ordinal form (e.g., 1st,
/// 2nd, etc.). The ordinal suffixes for each number can be set using the locale
/// key `LOCALE_ORDINAL_SUFFIXES`. The following are valid: `%Od`, `%Oe`, `%OH`,
/// `%OI`, `%Om`, `%OM`, `%OS`, `%Ou`, `%Ow`, `%Oy`, and `%OY`.
///
/// ## Flag Characters
/// Between the `%` character and the conversion specifier character, an
/// optional *flag* and *field width* may be specified. (These should precede
/// the `E` or `O` characters, if present).
///
/// The following flag characters are permitted:
/// - `_`: (underscore) Pad a numeric result string with spaces.
/// - `-`: (dash) Do not pad a numeric result string.
/// - `0`: Pad a numeric result string with zeroes even if the conversion
///        specifier character uses space-padding by default.
/// - `^`: Convert alphabetic characters in the result string to uppercase.
/// - `+`: Display a `-` before numeric values if the Time is negative, or a `+`
///        if the Time is positive or 0.
/// - `,`: Add comma separators for long numeric values.
///
/// An optional decimal width specifier may follow the (possibly absent) flag.
/// If the natural size of the field is smaller than this width, the result
/// string is padded (on the left) to the specified width. The string is never
/// truncated.
///
/// ## Examples
///
/// ```nwscript
/// struct Time t = StringToTime("1372-06-01 13:00:00:000");
///
/// // Default formatting
/// FormatDateTime(t); // "1372-06-01 13:00:00:000"
/// FormatDate(t); // "1372-06-01"
/// FormatTime(t); // "13:00:00:000"
///
/// // Using custom formats
/// FormatTime(t, "Today is %A, %B %Od."); // "Today is Monday, June 1st."
/// FormatTime(t, "%I:%M %p"); // "01:00 PM"
/// FormatTime(t, "%-I:%M %p"); // "1:00 PM"
/// ```
/// ----------------------------------------------------------------------------
/// # Advanced Usage
///
/// ## Locales
///
/// A locale is a json object that contains localization settings for formatting
/// functions. A default locale will be constructed using the configuration
/// values in `util_c_times.nss`, but you can also construct locales yourself.
/// An application for this might be having different areas in the module use
/// different month or day names, etc.
///
/// A locale is a simple json object:
/// ```nwscript
/// json jLocale = JsonObject();
/// ```
///
/// Alternatively, you can initialize a locale with the default values from
/// util_c_times:
/// ```nwscript
/// json jLocale = NewLocale();
/// ```
///
/// Keys are then added using `SetLocaleString()`:
/// ```nwscript
/// jLocale = SetLocaleString(jLocale, LOCALE_DAYS, "Moonday, Treeday, etc.");
/// ```
///
/// Keys can be retrieved using `GetLocaleString()`, which takes an optional
/// default value if the key is not set:
/// ```nwscript
/// string sDays     = GetLocaleString(jLocale, LOCALE_DAYS);
/// string sDaysAbbr = GetLocaleString(jLocale, LOCALE_DAYS_ABBR, sDays);
/// ```
///
/// Locales can be saved with a name. That names can then be passed to
/// formatting functions:
/// ```nwscript
/// json jLocale = JsonObject();
/// jLocale = SetLocaleString(jLocale, LOCALE_DAYS, "Moonday, Treeday, Heavensday, Valarday, Shipday, Starday, Sunday");
/// jLocale = SetLocaleString(jLocale, LOCALE_MONTHS, "Narvinye, Nenime, Sulime, Varesse, Lotesse, Narie, Cermie, Urime, Yavannie, Narquelie, Hisime, Ringare");
/// SetLocale(jLocale, "ME");
/// FormatTime(t, "Today is %A, %B %Od.");       // "Today is Monday, June 1st
/// FormatTime(t, "Today is %A, %B %Od.", "ME"); // "Today is Moonday, Narie 1st
/// ```
///
/// You can change the default locale so that you don't have to pass the name
/// every time:
/// ```nwscript
/// SetDefaultLocale("ME");
/// FormatTime(t, "Today is %A, %B %Od."); // "Today is Moonday, Narie 1st
/// ```
///
/// The following keys are currently supported:
/// - `LOCALE_DAYS`: a CSV list of 7 weekday names. Accessed by `%A`.
/// - `LOCALE_DAYS_ABBR`: a CSV list of 7 abbreviated weekday names. If not set,
///    the `FormatTime()` function will use `LOCALE_DAYS` instead. Accessed by
///    `%a`.
/// - `LOCALE_MONTHS`: a CSV list of 12 month names. Accessed by `%B`.
/// - `LOCALE_MONTHS_ABBR`: a CSV list of 12 abbreviated month names. If not
///   set, the `FormatTime()` function will use `LOCALE_MONTHS` instead.
///   Accessed by `%b`.
/// - `LOCALE_AMPM`: a CSV list of 2 AM/PM elements. Accessed by `%p` and `%P`.
/// - `LOCALE_ORDINAL_SUFFIXES`: a CSV list of suffixes for constructing ordinal
///   numbers. See util_c_times's documentation of `DEFAULT_ORDINAL_SUFFIXES`
///   for details.
/// - `LOCALE_DATETIME_FORMAT`: a date and time format string. Aliased by `%c`.
/// - `LOCALE_DATE_FORMAT`: a date format string. Aliased by `%x`.
/// - `LOCALE_TIME_FORMAT`: a time format string. Aliased by `%X`.
/// - `LOCALE_AMPM_FORMAT`: a time format string using AM/PM form. Aliased
///   by `%r`.
/// - `ERA_DATETIME_FORMAT`: a format string to display the date and time. If
///   not set, will fall back to `LOCALE_DATETIME_FORMAT`. Aliased by `%Ec`.
/// - `ERA_DATE_FORMAT`: a format string to display the date without the time.
///   If not set, will fall back to `LOCALE_DATE_FORMAT`. Aliased by `%Ex`.
/// - `ERA_TIME_FORMAT`: a format string to display the time without the date.
///   If not set, will fall back to `LOCALE_TIME_FORMAT`. Aliased by `%EX`.
/// - `ERA_YEAR_FORMAT`: a format string to display the year. If not set, will
///   display the year. Aliased by `%EY`.
/// - `ERA_NAME`: the name of an era. If not set and no era matches the current
///   year, will display the century. Aliased by `%EC`.
///
/// ## Eras
/// Locales can also hold an array of eras. Eras are json objects which name a
/// time range. When formatting using the `%E` modifier, the start Times of each
/// era in the array are compared to the Time to be formatted; the era with the
/// latest start that is still before the Time is selected. Format codes can
/// then refer to the era's name, year relative to the era start, and other
/// era-specific formats.
///
/// An era can be created using `DefineEra()`. This function takes a name and a
/// start Time. See the documentation for `DefineEra()` for further info:
/// ```nwscript
/// // Create an era that begins at the first possible calendar time
/// json jFirst = DefineEra("First Age", GetTime());
///
/// // Create an era that begins on a particular year
/// json jSecond = DefineEra("Second Age", GetTime(590));
/// ```
///
/// The `{Get/Set}LocaleString()` functions also apply to eras:
/// ```nwscript
/// jSecond = SetLocaleString(jSecond, ERA_DATETIME_FORMAT, "%B %Od, %EY");
/// jSecond = SetLocaleString(jSecond, ERA_YEAR_FORMAT, "%EY 2E");
/// ```
///
/// You can add an era to a locale using `AddEra()`:
/// ```nwscript
/// json jLocale = GetLocale("ME");
/// jLocale = SetLocaleString(jLocale, LOCALE_DAYS, "Moonday, Treeday, Heavensday, Valarday, Shipday, Starday, Sunday");
/// jLocale = SetLocaleString(jLocale, LOCALE_MONTHS, "Narvinye, Nenime, Sulime, Varesse, Lotesse, Narie, Cermie, Urime, Yavannie, Narquelie, Hisime, Ringare");
/// jLocale = AddEra(jLocale, jFirst);
/// jLocale = AddEra(jLocale, jSecond);
/// SetLocale(jLocale, "ME");
/// ```
///
/// You can then access the era settings using the `%E` modifier:
/// ```nwscript
/// FormatTime(t, "Today is %A, %B %Od, %EY.", "ME"); // "Today is Moonday, Narie 1st, 783 2E."
///
/// // You can combine the `%E` and `%O` modifiers
/// FormatTime(t, "It is the %EOy year of the %EC.", "ME"); // "It is the 783rd year of the Second Age."
/// ```
///
/// The following keys are available to eras:
/// - `ERA_NAME`: the name of the era. Aliased by `%EC`.
/// - `ERA_DATETIME_FORMAT`: a format string to display the date and time. If
///   not set, will fall back to the value on the locale. Aliased by `%Ec`.
/// - `ERA_DATE_FORMAT`: a format string to display the date without the time.
///   If not set, will fall back to the value on the locale. Aliased by `%Ex`.
/// - `ERA_TIME_FORMAT`: a format string to display the time without the date.
///   If not set, will fall back to the value on the locale. Aliased by `%EX`.
/// - `ERA_YEAR_FORMAT`: a format string to display the year. Defaults to
///   `%Ey %EC`. If not set, will fall back to the value on the locale. Aliased
///   by `%EY`.
/// ----------------------------------------------------------------------------

#include "util_i_times"
#include "util_i_csvlists"
#include "util_c_strftime"

// -----------------------------------------------------------------------------
//                                   Constants
// -----------------------------------------------------------------------------

// These are the characters used as flags in time format codes.
const string TIME_FLAG_CHARS = "EO^,+-_0123456789";

const int TIME_FLAG_ERA       = 0x01; ///< `E`: use era-based formatting
const int TIME_FLAG_ORDINAL   = 0x02; ///< `O`: use ordinal numbers
const int TIME_FLAG_UPPERCASE = 0x04; ///< `^`: use uppercase letters
const int TIME_FLAG_COMMAS    = 0x08; ///< `,`: add comma separators
const int TIME_FLAG_SIGN      = 0x10; ///< `+`: prefix with sign character
const int TIME_FLAG_NO_PAD    = 0x20; ///< `-`: do not pad numbers
const int TIME_FLAG_SPACE_PAD = 0x40; ///< `_`: pad numbers with spaces
const int TIME_FLAG_ZERO_PAD  = 0x80; ///< `0`: pad numbers with zeros

// These are the characters allowed in time format codes.
const string TIME_FORMAT_CHARS = "aAbBpPIljwuCyYmdeHkMSfDFRTcxXr%";

// Begin time-only constants. It is an error to use these with a duration.
const int TIME_FORMAT_NAME_OF_DAY_ABBR   =  0; ///< `%a`: Mon..Sun
const int TIME_FORMAT_NAME_OF_DAY_LONG   =  1; ///< `%A`: Monday..Sunday
const int TIME_FORMAT_NAME_OF_MONTH_ABBR =  2; ///< `%b`: Jan..Dec
const int TIME_FORMAT_NAME_OF_MONTH_LONG =  3; ///< `%B`: January..December
const int TIME_FORMAT_AMPM_UPPER         =  4; ///< `%p`: AM..PM
const int TIME_FORMAT_AMPM_LOWER         =  5; ///< `%P`: am..pm
const int TIME_FORMAT_HOUR_12            =  6; ///< `%I`: 01..12
const int TIME_FORMAT_HOUR_12_SPACE_PAD  =  7; ///< `%l`: alias for %_I
const int TIME_FORMAT_DAY_OF_YEAR        =  8; ///< `%j`: 001..336
const int TIME_FORMAT_DAY_OF_WEEK_0_6    =  9; ///< `%w`: weekdays 0..6
const int TIME_FORMAT_DAY_OF_WEEK_1_7    = 10; ///< `%u`: weekdays 1..7
const int TIME_FORMAT_YEAR_CENTURY       = 11; ///< `%C`: 0..320
const int TIME_FORMAT_YEAR_SHORT         = 12; ///< `%y`: 00..99
const int TIME_FORMAT_YEAR_LONG          = 13; ///< `%Y`: 0..320000
const int TIME_FORMAT_MONTH              = 14; ///< `%m`: 01..12
const int TIME_FORMAT_DAY                = 15; ///< `%d`: 01..28
const int TIME_FORMAT_DAY_SPACE_PAD      = 16; ///< `%e`: alias for %_d
const int TIME_FORMAT_HOUR_24            = 17; ///< `%H`: 00..23
const int TIME_FORMAT_HOUR_24_SPACE_PAD  = 18; ///< `%k`: alias for %_H
const int TIME_FORMAT_MINUTE             = 19; ///< `%M`: 00..59 (depending on conversion factor)
const int TIME_FORMAT_SECOND             = 20; ///< `%S`: 00..59
const int TIME_FORMAT_MILLISECOND        = 21; ///< `%f`: 000...999
const int TIME_FORMAT_DATE_US            = 22; ///< `%D`: 06/01/72
const int TIME_FORMAT_DATE_ISO           = 23; ///< `%F`: 1372-06-01
const int TIME_FORMAT_TIME_US            = 24; ///< `%R`: 13:00
const int TIME_FORMAT_TIME_ISO           = 25; ///< `%T`: 13:00:00
const int TIME_FORMAT_LOCALE_DATETIME    = 26; ///< `%c`: locale-specific date and time
const int TIME_FORMAT_LOCALE_DATE        = 27; ///< `%x`: locale-specific date
const int TIME_FORMAT_LOCALE_TIME        = 28; ///< `%X`: locale-specific time
const int TIME_FORMAT_LOCALE_TIME_AMPM   = 29; ///< `%r`: locale-specific AM/PM time
const int TIME_FORMAT_PERCENT            = 30; ///< `%%`: %

// Time format codes with an index less than this number are not valid for
// durations.
const int DURATION_FORMAT_OFFSET = TIME_FORMAT_YEAR_CENTURY;

// ----- VarNames --------------------------------------------------------------

// Prefix for locale names stored on the module to avoid collision
const string LOCALE_PREFIX = "*Locale: ";

// Stores the default locale on the module
const string LOCALE_DEFAULT = "*DefaultLocale";

// Each of these keys stores a CSV list which is evaluated by a format code
const string LOCALE_DAYS        = "Days";       // day names (%A)
const string LOCALE_DAYS_ABBR   = "DaysAbbr";   // abbreviated day names (%a)
const string LOCALE_MONTHS      = "Months";     // month names (%B)
const string LOCALE_MONTHS_ABBR = "MonthsAbbr"; // abbreviated month names (%b)
const string LOCALE_AMPM        = "AMPM";       // AM/PM elements (%p and %P)

// This key stores a CSV list of suffixes used to convert integers to ordinals
// (e.g., 0th, 1st, etc.).
const string LOCALE_ORDINAL_SUFFIXES = "OrdinalSuffixes"; // %On

// Each of these keys stores a locale-specific format string which is aliased by
// a format code.
const string LOCALE_DATETIME_FORMAT  = "DateTimeFormat"; // %c
const string LOCALE_DATE_FORMAT      = "DateFormat";     // %x
const string LOCALE_TIME_FORMAT      = "TimeFormat";     // %X
const string LOCALE_AMPM_FORMAT      = "AMPMFormat";     // %r

// Each of these keys stores a locale-specific era-based format string which is
// aliased by a format code using the `E` modifier. If no string is stored at
// this key, it will resolve to the non-era based format above.
const string ERA_DATETIME_FORMAT = "EraDateTimeFormat"; // %Ec
const string ERA_DATE_FORMAT     = "EraDateFormat";     // %Ex
const string ERA_TIME_FORMAT     = "EraTimeFormat";     // %EX

// Key for Eras json array. Each element of the array is a json object having
// the three keys below.
const string LOCALE_ERAS = "Eras";

// Key for era name. Aliased by %EC.
const string ERA_NAME = "Name";

// Key for a format string for the year in the era. Aliased by %EY.
const string ERA_YEAR_FORMAT = "YearFormat";

// Key for the start of the era. Stored as a date in the form yyyy-mm-dd.
const string ERA_START = "Start";

// Key for the number of the year closest to the start date in an era. Used by
// %Ey to display the correct year. For example, if an era starts on 1372-01-01
// and the current date is 1372-06-01, an offset of 0 would make %Ey display 0,
// while an offset of 1 would make it display 1.
const string ERA_OFFSET = "Offset";


// -----------------------------------------------------------------------------
//                              Function Prototypes
// -----------------------------------------------------------------------------

// ----- Locales ---------------------------------------------------------------

/// @brief Get the string at a given key in a locale object.
/// @param jLocale A json object containing the locale settings
/// @param sKey The key to return the value of (see the LOCALE_* constants)
/// @param sDefault A default value to return if sKey does not exist in jLocale.
string GetLocaleString(json jLocale, string sKey, string sSuffix = "");

/// @brief Set the string at a given key in a locale object.
/// @param jLocale A json object containing the locale settings
/// @param sKey The key to set the value of (see the LOCALE_* constants)
/// @param sValue The value to set the key to
/// @returns The updated locale object
json SetLocaleString(json j, string sKey, string sValue);

/// @brief Create a new locale object initialized with values from util_c_times.
/// @note If you do not want the default values, use JsonObject() instead.
json NewLocale();

/// @brief Get the name of the default locale for the module.
/// @returns The name of the default locale, or the value of DEFAULT_LOCALE from
///     util_c_times.nss if a locale is not set.
string GetDefaultLocale();

/// @brief Set the name of the default locale for the module.
/// @param sName The name of the locale (default: DEFAULT_LOCALE)
void SetDefaultLocale(string sName = DEFAULT_LOCALE);

/// @brief Get a locale object by name.
/// @param sLocale The name of the locale. Will return the default locale if "".
/// @param bInit If TRUE, will return an era with the default values from
///     util_c_times.nss if sLocale does not exist.
/// @returns A json object containing the locale settings, or JsonNull() if no
///     locale named sLocale exists.
json GetLocale(string sLocale = "", int bInit = TRUE);

/// @brief Save a locale object to a name.
/// @param jLocale A json object containing the locale settings.
/// @param sLocale The name of the locale. Will use the default local if "".
void SetLocale(json jLocale, string sLocale = "");

/// @brief Delete a locale by name.
/// @param sLocale The name of the locale. Will use the default local if "".
void DeleteLocale(string sLocale = "");

/// @brief Check if a locale exists.
/// @param sLocale The name of the locale. Will use the default local if "".
/// @returns TRUE if sLocale points to a valid json object, other FALSE.
int HasLocale(string sLocale = "");

/// @brief Get the name of a month given a locale.
/// @param nMonth The month of the year (1-indexed).
/// @param sMonths A CSV list of 12 month names to search through. If "", will
///     use the month list from a locale.
/// @param sLocale The name of a locale to check for month names if sMonths is
///     "". If sLocale is "", will use the default locale.
/// @returns The name of the month.
string MonthToString(int nMonth, string sMonths = "", string sLocale = "");

/// @brief Get the name of a day given a locale.
/// @param nDay The day of the week (1-indexed).
/// @param sDays A CSV list of 7 day names to search through. If "", will use
///     the day list from a locale.
/// @param sLocale The name of a locale to check for day names if sDays is "".
///     If sLocale is "", will use the default locale.
/// @returns The name of the day.
string DayToString(int nDay, string sDays = "", string sLocale = "");

// ----- Eras ------------------------------------------------------------------

/// @brief Create an era json object.
/// @param sName The name of the era.
/// @param tStart The Time marking the beginning of the era.
/// @param nOffset The number that represents the first year in an era. Used by
///     %Ey to display the correct year. For example, if an era starts on
///     1372-01-01 and the current date is 1372-06-01, an offset of 0 would make
///     %Ey display 0 while an offset of 1 would make %Ey display 1. The default
///     is 0 since NWN allows year 0.
/// @param sFormat The default format for an era-based year. The format code %EY
///     evaluates to this string for this era. With the default value, the 42nd
///     year of an era named "Foo" would be "4 Foo".
json DefineEra(string sName, struct Time tStart, int nOffset = 0, string sFormat = "%Ey %EC");

/// @brief Add an era to a locale.
/// @param jLocale A locale json object.
/// @param jEra An era json object.
/// @returns A modified copy of jLocale with jEra added to its era array.
json AddEra(json jLocale, json jEra);

/// @brief Get the era in which a time occurs.
/// @param jLocale A locale json object containing an array of eras.
/// @param t A Time to check the era for.
/// @returns A json object for the era in jLocale with the latest start time
///     earlier than t or JsonNull() if no such era is present.
json GetEra(json jLocale, struct Time t);

/// @brief Get the year of an era given an NWN calendar year.
/// @param jEra A json object matching an era.
/// @param nYear An NWN calendar year (0..32000)
/// @returns The number of the year in the era, or nYear if jEra is not valid.
int GetEraYear(json jEra, int nYear);

/// @brief Gets a string from an era, falling back to a locale if not set.
/// @param jEra The era to check
/// @param jLocale The locale to fall back to
/// @param sKey The key to get the string from
/// @note If sKey begins with "Era" and was not found on the era or the locale,
///     will check jLocale for sKey without the "Era" prefix.
string GetEraString(json jEra, json jLocale, string sKey);

// ----- Formatting ------------------------------------------------------------

/// @brief Convert an integer into an ordinal number (e.g., 1 -> 1st, 2 -> 2nd).
/// @param n The number to convert.
/// @param sSuffixes A CSV list of suffixes for each integer, starting at 0. If
///     the n <= the length of the list, only the last digit will be checked. If
///     "", will use the suffixes provided by the locale instead.
/// @param sLocale The name of the locale to use when formatting the number. If
///     "", will use the default locale.
string IntToOrdinalString(int n, string sSuffixes = "", string sLocale = "");

/// @brief Format a Time into a string.
/// @param t A calendar or duration Time to format. No conversion is performed.
/// @param sFormat A string containing format codes to control the output.
/// @param sLocale The name of the locale to use when formatting the time. If
///     "", will use the default locale.
/// @note See the documentation at the top of this file for the list of possible
///     format codes.
string strftime(struct Time t, string sFormat, string sLocale = "");

/// @brief Format a calendar Time into a string.
/// @param t A calendar Time to format. If not a calendar Time, will be
///     converted into one.
/// @param sFormat A string containing format codes to control the output. The
///     default value is equivalent to "%H:%M:%S".
/// @param sLocale The name of the locale to use when formatting the time. If
///     "", will use the default locale.
/// @note This function differs only from FormatTime() in the default value of
///     sFormat. Character codes that apply to calendar Times are still valid.
/// @note See the documentation at the top of this file for the list of possible
///     format codes.
string FormatTime(struct Time t, string sFormat = "%X", string sLocale = "");

/// @brief Format a calendar Time into a string.
/// @param t A calendar Time to format. If not a calendar Time, will be
///     converted into one.
/// @param sFormat A string containing format codes to control the output. The
///     default value is equivalent to "%Y-%m-%d".
/// @param sLocale The name of the locale to use when formatting the date. If
///     "", will use the default locale.
/// @note This function differs only from FormatTime() in the default value of
///     sFormat. Character codes that apply to calendar Times are still valid.
/// @note See the documentation at the top of this file for the list of possible
///     format codes.
string FormatDate(struct Time t, string sFormat = "%x", string sLocale = "");

/// @brief Format a calendar Time into a string.
/// @param t A calendar Time to format. If not a calendar Time, will be
///     converted into one.
/// @param sFormat A string containing format codes to control the output. The
///     default value is equivalent to "%Y-%m-%d %H:%M:%S:%f".
/// @param sLocale The name of the locale to use when formatting the Time. If
///     "", will use the default locale.
/// @note This function differs only from FormatTime() in the default value of
///     sFormat. Character codes that apply to calendar Times are still valid.
/// @note See the documentation at the top of this file for the list of possible
///     format codes.
string FormatDateTime(struct Time t, string sFormat = "%c", string sLocale = "");

/// @brief Format a duration Time into a string.
/// @param t The duration Time to format. If not a duration Time, will be
///     converted into one.
/// @param sFormat A string containing format codes to control the output. The
///     default value is equivalent to ISO 8601 format preceded by the sign of
///     t (`-` if negative, `+` otherwise).
/// @param sLocale The name of the locale to use when formatting the duration.
///     If "", will use the default locale.
/// @note See the documentation at the top of this file for the list of possible
///     format codes.
string FormatDuration(struct Time t, string sFormat = "%+Y-%m-%d %H:%M:%S:%f", string sLocale = "");

// -----------------------------------------------------------------------------
//                             Function Definitions
// -----------------------------------------------------------------------------

// ----- Locales ---------------------------------------------------------------

string GetLocaleString(json jLocale, string sKey, string sDefault = "")
{
    json jElem = JsonObjectGet(jLocale, sKey);
    if (JsonGetType(jElem) == JSON_TYPE_STRING && JsonGetString(jElem) != "")
        return JsonGetString(jElem);
    return sDefault;
}

json SetLocaleString(json j, string sKey, string sValue)
{
    return JsonObjectSet(j, sKey, JsonString(sValue));
}

json NewLocale()
{
    json j = JsonObject();
    j = SetLocaleString(j, LOCALE_ORDINAL_SUFFIXES, DEFAULT_ORDINAL_SUFFIXES);
    j = SetLocaleString(j, LOCALE_DAYS,             DEFAULT_DAYS);
    j = SetLocaleString(j, LOCALE_DAYS_ABBR,        DEFAULT_DAYS_ABBR);
    j = SetLocaleString(j, LOCALE_MONTHS,           DEFAULT_MONTHS);
    j = SetLocaleString(j, LOCALE_MONTHS_ABBR,      DEFAULT_MONTHS_ABBR);
    j = SetLocaleString(j, LOCALE_AMPM,             DEFAULT_AMPM);
    j = SetLocaleString(j, LOCALE_DATETIME_FORMAT,  DEFAULT_DATETIME_FORMAT);
    j = SetLocaleString(j, LOCALE_DATE_FORMAT,      DEFAULT_DATE_FORMAT);
    j = SetLocaleString(j, LOCALE_TIME_FORMAT,      DEFAULT_TIME_FORMAT);
    j = SetLocaleString(j, LOCALE_AMPM_FORMAT,      DEFAULT_AMPM_FORMAT);

    if (DEFAULT_ERA_DATETIME_FORMAT != "")
        j = SetLocaleString(j, ERA_DATETIME_FORMAT, DEFAULT_ERA_DATETIME_FORMAT);

    if (DEFAULT_ERA_DATE_FORMAT != "")
        j = SetLocaleString(j, ERA_DATE_FORMAT, DEFAULT_ERA_DATE_FORMAT);

    if (DEFAULT_ERA_TIME_FORMAT != "")
        j = SetLocaleString(j, ERA_TIME_FORMAT, DEFAULT_ERA_TIME_FORMAT);

    if (DEFAULT_ERA_NAME != "")
        j = SetLocaleString(j, ERA_NAME, DEFAULT_ERA_NAME);

    return JsonObjectSet(j, LOCALE_ERAS, JsonArray());
}

string GetDefaultLocale()
{
    string sLocale = GetLocalString(GetModule(), LOCALE_DEFAULT);
    return sLocale == "" ? DEFAULT_LOCALE : sLocale;
}

void SetDefaultLocale(string sName = DEFAULT_LOCALE)
{
    SetLocalString(GetModule(), LOCALE_DEFAULT, sName);
}

json GetLocale(string sLocale = "", int bInit = TRUE)
{
    if (sLocale == "")
        sLocale = GetDefaultLocale();
    json j = GetLocalJson(GetModule(), LOCALE_PREFIX + sLocale);
    if (bInit && JsonGetType(j) != JSON_TYPE_OBJECT)
        j = NewLocale();
    return j;
}

void SetLocale(json jLocale, string sLocale = "")
{
    if (sLocale == "")
        sLocale = GetDefaultLocale();
    SetLocalJson(GetModule(), LOCALE_PREFIX + sLocale, jLocale);
}

void DeleteLocale(string sLocale = "")
{
    if (sLocale == "")
        sLocale = GetDefaultLocale();
    DeleteLocalJson(GetModule(), LOCALE_PREFIX + sLocale);
}

int HasLocale(string sLocale = "")
{
    return JsonGetType(GetLocale(sLocale, FALSE)) == JSON_TYPE_OBJECT;
}

string MonthToString(int nMonth, string sMonths = "", string sLocale = "")
{
    if (sMonths == "")
        sMonths = GetLocaleString(GetLocale(sLocale), LOCALE_MONTHS);

    return GetListItem(sMonths, (nMonth - 1) % 12);
}

string DayToString(int nDay, string sDays = "", string sLocale = "")
{
    if (sDays == "")
        sDays = GetLocaleString(GetLocale(sLocale), LOCALE_DAYS);

    return GetListItem(sDays, (nDay - 1) % 7);
}

// ----- Eras ------------------------------------------------------------------

json DefineEra(string sName, struct Time tStart, int nOffset = 0, string sFormat = DEFAULT_ERA_YEAR_FORMAT)
{
    json jEra = JsonObject();
    jEra = JsonObjectSet(jEra, ERA_NAME,        JsonString(sName));
    jEra = JsonObjectSet(jEra, ERA_YEAR_FORMAT, JsonString(sFormat));
    jEra = JsonObjectSet(jEra, ERA_START,       TimeToJson(tStart));
    return JsonObjectSet(jEra, ERA_OFFSET,      JsonInt(nOffset));
}

json AddEra(json jLocale, json jEra)
{
    json jEras = JsonObjectGet(jLocale, LOCALE_ERAS);
    if (JsonGetType(jEras) != JSON_TYPE_ARRAY)
        jEras = JsonArray();

    jEras = JsonArrayInsert(jEras, jEra);
    return JsonObjectSet(jLocale, LOCALE_ERAS, jEras);
}

json GetEra(json jLocale, struct Time t)
{
    if (t.Type == TIME_TYPE_DURATION)
        return JsonNull();

    json  jEras = JsonObjectGet(jLocale, LOCALE_ERAS);
    json  jEra; // The closest era to the Time
    struct Time tEra; // The start Time of jEra
    int i, nLength = JsonGetLength(jEras);

    for (i = 0; i < nLength; i++)
    {
        json jCmp = JsonArrayGet(jEras, i);
        struct Time tCmp = JsonToTime(JsonObjectGet(jCmp, ERA_START));
        switch (CompareTime(t, tCmp))
        {
            case 0: return jCmp;
            case 1:
            {
                if (CompareTime(tCmp, tEra) >= 0)
                {
                    tEra = tCmp;
                    jEra = jCmp;
                }
            }
        }
    }

    return jEra;
}

int GetEraYear(json jEra, int nYear)
{
    int nOffset = JsonGetInt(JsonObjectGet(jEra, ERA_OFFSET));
    struct Time tStart = JsonToTime(JsonObjectGet(jEra, ERA_START));
    return nYear - tStart.Year + nOffset;
}

string GetEraString(json jEra, json jLocale, string sKey)
{
    json jValue = JsonObjectGet(jEra, sKey);
    if (JsonGetType(jValue) != JSON_TYPE_STRING)
    {
        jValue = JsonObjectGet(jLocale, sKey);
        if (JsonGetType(jValue) != JSON_TYPE_STRING &&
           (GetStringSlice(sKey, 0, 2) == "Era"))
            jValue = JsonObjectGet(jLocale, GetStringSlice(sKey, 3));
    }

    return JsonGetString(jValue);
}

// ----- Formatting ------------------------------------------------------------

string IntToOrdinalString(int n, string sSuffixes = "", string sLocale = "")
{
    if (sSuffixes == "")
    {
        json jLocale = GetLocale(sLocale);
        sSuffixes = GetLocaleString(jLocale, LOCALE_ORDINAL_SUFFIXES, DEFAULT_ORDINAL_SUFFIXES);
    }

    int nIndex = abs(n) % 100;
    if (nIndex >= CountList(sSuffixes))
        nIndex = abs(n) % 10;

    return IntToString(n) + GetListItem(sSuffixes, nIndex);
}

string strftime(struct Time t, string sFormat, string sLocale)
{
    int  nOffset, nPos;
    int  nSign   = GetTimeSign(t);
    json jValues = JsonArray();
    json jLocale = GetLocale(sLocale);
    json jEra    = GetEra(jLocale, t);
    string sOrdinals = GetLocaleString(jLocale, LOCALE_ORDINAL_SUFFIXES, DEFAULT_ORDINAL_SUFFIXES);
    int nDigitsIndex = log2(TIME_FLAG_ZERO_PAD);

    while ((nPos = FindSubString(sFormat, "%", nOffset)) != -1)
    {
        nOffset = nPos;

        // Check for flags
        int nFlag, nFlags;
        string sPadding, sWidth, sChar;

        while ((nFlag = FindSubString(TIME_FLAG_CHARS, (sChar = GetChar(sFormat, ++nPos)))) != -1)
        {
            // If this character is not a digit after 0, we create a flag for it
            // and add it to our list of flags.
            if (nFlag < nDigitsIndex)
                nFlags |= (1 << nFlag);
            else
            {
                // The user has specified a width for the item. Parse all the
                // numbers.
                sWidth = ""; // in case the user added a width twice and separated with another flag.
                while (GetIsNumeric(sChar))
                {
                    sWidth += sChar;
                    sChar = GetChar(sFormat, ++nPos);
                }

                nPos--;
            }
        }

        string sValue;
        int nValue;
        int bAllowEmpty;
        int nPadding = 2; // Most numeric formats use this

        // We offset where we start looking for format codes based on whether
        // this is a calendar Time or duration Time. Durations cannot use time
        // codes that only make sense in the context of a calendar Time.
        int nFormat = FindSubString(TIME_FORMAT_CHARS, sChar, t.Type ? 0 : DURATION_FORMAT_OFFSET);
        switch (nFormat)
        {
            case -1:
            {
                string sError = GetStringSlice(sFormat, nOffset, nPos);
                string sColored = GetStringSlice(sFormat, 0, nOffset - 1) +
                                  HexColorString(sError, COLOR_RED) +
                                  GetStringSlice(sFormat, nPos + 1);
                Error("Illegal time format \"" + sError + "\": " + sColored);
                sFormat = ReplaceSubString(sFormat, "%" + sError, nOffset, nPos);
                continue;
            }

            // Note that some of these are meant to fall through
            case TIME_FORMAT_DAY_SPACE_PAD: // %e
                sPadding = " ";
            case TIME_FORMAT_DAY: // %d
                nValue = t.Day;
                break;
            case TIME_FORMAT_HOUR_24_SPACE_PAD: // %H
                sPadding = " ";
            case TIME_FORMAT_HOUR_24: // %H
                nValue = t.Hour;
                break;
            case TIME_FORMAT_HOUR_12_SPACE_PAD: // %l
                sPadding = " ";
            case TIME_FORMAT_HOUR_12: // %I
                nValue = t.Hour > 12 ? t.Hour % 12 : t.Hour;
                nValue = nValue ? nValue : 12;
                break;
            case TIME_FORMAT_MONTH: // %m
                nValue = t.Month;
                break;
            case TIME_FORMAT_MINUTE: // %M
                nValue = t.Minute;
                break;
            case TIME_FORMAT_SECOND: // %S
                nValue = t.Second;
                break;
            case TIME_FORMAT_MILLISECOND: // %f
                nValue = t.Millisecond;
                nPadding = 3;
                break;
            case TIME_FORMAT_DAY_OF_YEAR: // %j
                nValue = t.Month * 28 + t.Day;
                nPadding = 3;
                break;
            case TIME_FORMAT_DAY_OF_WEEK_0_6: // %w
                nValue = t.Day % 7;
                nPadding = 1;
                break;
            case TIME_FORMAT_DAY_OF_WEEK_1_7: // %u
                nValue = (t.Day % 7) + 1;
                nPadding = 1;
                break;
            case TIME_FORMAT_AMPM_UPPER: // %p
            case TIME_FORMAT_AMPM_LOWER: // %P
                bAllowEmpty = TRUE;
                sValue = GetLocaleString(jLocale, LOCALE_AMPM);
                sValue = GetListItem(sValue, t.Hour % 24 >= 12);
                if (nFormat == TIME_FORMAT_AMPM_LOWER)
                    sValue = GetStringLowerCase(sValue);
                break;
            case TIME_FORMAT_NAME_OF_DAY_LONG: // %A
                bAllowEmpty = TRUE;
                sValue = GetLocaleString(jLocale, LOCALE_DAYS);
                sValue = DayToString(t.Day, sValue);
                break;
            case TIME_FORMAT_NAME_OF_DAY_ABBR: // %a
                bAllowEmpty = TRUE;
                sValue = GetLocaleString(jLocale, LOCALE_DAYS);
                sValue = GetLocaleString(jLocale, LOCALE_DAYS_ABBR, sValue);
                sValue = DayToString(t.Day, sValue);
                break;
            case TIME_FORMAT_NAME_OF_MONTH_LONG: // %B
                bAllowEmpty = TRUE;
                sValue = GetLocaleString(jLocale, LOCALE_MONTHS);
                sValue = MonthToString(t.Month, sValue);
                break;
            case TIME_FORMAT_NAME_OF_MONTH_ABBR: // %b
                bAllowEmpty = TRUE;
                sValue = GetLocaleString(jLocale, LOCALE_MONTHS);
                sValue = GetLocaleString(jLocale, LOCALE_MONTHS_ABBR, sValue);
                sValue = MonthToString(t.Month, sValue);
                break;

            // We handle literal % here instead of replacing it directly because
            // we want the user to be able to pad it if desired.
            case TIME_FORMAT_PERCENT: // %%
                sValue = "%";
                break;

            case TIME_FORMAT_YEAR_CENTURY: // %C, %EC
                if (nFlags & TIME_FLAG_ERA)
                    sValue = GetEraString(jEra, jLocale, ERA_NAME);
                nValue = t.Year / 100;
                break;
            case TIME_FORMAT_YEAR_SHORT: // %y, %Ey
                nValue = (nFlags & TIME_FLAG_ERA) ? GetEraYear(jEra, t.Year) : t.Year % 100;
                break;

            case TIME_FORMAT_YEAR_LONG: // %Y, %EY
                if (nFlags & TIME_FLAG_ERA)
                {
                    sValue = GetEraString(jEra, jLocale, ERA_YEAR_FORMAT);
                    if (sValue != "")
                    {
                        sFormat = ReplaceSubString(sFormat, sValue, nOffset, nPos);
                        continue;
                    }
                }

                nValue = t.Year;
                nPadding = 4;
                break;

            // These codes are shortcuts to common operations. We replace the
            // parsed code with the substitution and re-parse from the same
            // offset.
            case TIME_FORMAT_DATE_US: // %D
                sFormat = ReplaceSubString(sFormat, "%m/%d/%y", nOffset, nPos);
                continue;
            case TIME_FORMAT_DATE_ISO: // %F
                sFormat = ReplaceSubString(sFormat, "%Y-%m-%d", nOffset, nPos);
                continue;
            case TIME_FORMAT_TIME_US: // %R
                sFormat = ReplaceSubString(sFormat, "%H:%M", nOffset, nPos);
                continue;
            case TIME_FORMAT_TIME_ISO: // %T
                sFormat = ReplaceSubString(sFormat, "%H:%M:%S", nOffset, nPos);
                continue;
            case TIME_FORMAT_LOCALE_DATETIME: // %c, %Ec
                if (nFlags & TIME_FLAG_ERA)
                    sValue = GetEraString(jEra, jLocale, ERA_DATETIME_FORMAT);
                else
                    sValue = GetLocaleString(jLocale, LOCALE_DATETIME_FORMAT, DEFAULT_DATETIME_FORMAT);
                sFormat = ReplaceSubString(sFormat, sValue, nOffset, nPos);
                continue;
            case TIME_FORMAT_LOCALE_DATE: // %x, %Ex
                if (nFlags & TIME_FLAG_ERA)
                    sValue = GetEraString(jEra, jLocale, ERA_DATE_FORMAT);
                else
                    sValue = GetLocaleString(jLocale, LOCALE_DATE_FORMAT, DEFAULT_DATE_FORMAT);
                sFormat = ReplaceSubString(sFormat, sValue, nOffset, nPos);
                continue;
            case TIME_FORMAT_LOCALE_TIME: // %c, %Ec
                if (nFlags & TIME_FLAG_ERA)
                    sValue = GetEraString(jEra, jLocale, ERA_TIME_FORMAT);
                else
                    sValue = GetLocaleString(jLocale, LOCALE_TIME_FORMAT, DEFAULT_TIME_FORMAT);
                sFormat = ReplaceSubString(sFormat, sValue, nOffset, nPos);
                continue;
            case TIME_FORMAT_LOCALE_TIME_AMPM: // %r
                sValue = GetLocaleString(jLocale, LOCALE_AMPM_FORMAT, DEFAULT_AMPM_FORMAT);
                sFormat = ReplaceSubString(sFormat, sValue, nOffset, nPos);
                continue;
        }

        if ((sValue == "" && !bAllowEmpty) && (nFlags & TIME_FLAG_ORDINAL))
            sValue = IntToOrdinalString(nValue, sOrdinals);

        if (nFlags & TIME_FLAG_NO_PAD)
            sPadding = "";
        else if (sValue != "" || bAllowEmpty)
            sPadding = " " + sWidth;
        else
        {
            if (nFlags & TIME_FLAG_SPACE_PAD)
                sPadding = " ";
            else if (nFlags & TIME_FLAG_ZERO_PAD || sPadding == "")
                sPadding = "0";

            sPadding += sWidth != "" ? sWidth : IntToString(nPadding);
        }

        if (sValue != "" || bAllowEmpty)
        {
            if (nFlags & TIME_FLAG_UPPERCASE)
                sValue = GetStringUpperCase(sValue);
            jValues = JsonArrayInsert(jValues, JsonString(sValue));
            sFormat = ReplaceSubString(sFormat, "%" + sPadding + "s", nOffset, nPos);
        }
        else
        {
            if (nFlags & TIME_FLAG_SIGN)
                sValue = nSign < 0 ? "-" : "+";

            if (nFlags & TIME_FLAG_COMMAS)
                sPadding = "," + sPadding;

            jValues = JsonArrayInsert(jValues, JsonInt(abs(nValue)));
            sFormat = ReplaceSubString(sFormat, sValue + "%" + sPadding + "d", nOffset, nPos);
        }

        // Continue parsing from the end of the format string
        nOffset = nPos + GetStringLength(sPadding);
    }

    // Interpolate the values
    return FormatValues(jValues, sFormat);
}

string FormatTime(struct Time t, string sFormat = "%X", string sLocale = "")
{
    return strftime(DurationToTime(t), sFormat, sLocale);
}

string FormatDate(struct Time t, string sFormat = "%x", string sLocale = "")
{
    return strftime(DurationToTime(t), sFormat, sLocale);
}

string FormatDateTime(struct Time t, string sFormat = "%c", string sLocale = "")
{
    return strftime(DurationToTime(t), sFormat, sLocale);
}

string FormatDuration(struct Time t, string sFormat = "%+Y-%m-%d %H:%M:%S:%f", string sLocale = "")
{
    return strftime(TimeToDuration(t), sFormat, sLocale);
}