RoT2_PRC8/_module/nss/util_i_strftime.nss
Jaysyn904 379c5dce03 Added persistent player storage
Added persistent player storage.  Fixed store items.  Full compile.  Updated release archive.
2025-03-09 20:14:36 -04:00

1061 lines
48 KiB
Plaintext

/// ----------------------------------------------------------------------------
/// @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);
}