util_i_times.nss
contains utilities for dealing with times, dates, and
durations. util_i_times.nss
requires the following scripts:
util_i_debug.nss
util_i_math.nss
util_i_strings.nss
util_i_color.nss
util_c_color.nss
util_i_strftime.nss
contains advanced formatting functions for times; it
includes util_c_strftime.nss
which contains configuration settings to help
with formatting times to your liking. util_i_strftime.nss
includes the
following scripts:
util_i_debug.nss
util_i_math.nss
util_i_strings.nss
util_i_color.nss
util_c_color.nss
util_i_csvlists.nss
util_i_times.nss
util_c_strftime.nss
- Time: a struct value that may represent either a calendar time or a duration. A Time has a field for years, months, day, hours, minutes, seconds, and milliseconds. A Time also has a field to set the minutes per hour (defaults to the module setting, which defaults to 2).
- Calendar Time: A Time representing a particular moment in time as measured using the game calendar and clock. In a calendar Time, the month and day count from 1, while all other units count from 0 (including years, since NWN allows year 0). A calendar Time must always be positive.
- Duration Time: A Time representing an amount of time. All units in a
duration Time count from 0. A duration Time may be negative, representing
going back in time. This can be useful for calculations. A duration Time can
be converted to seconds to pass it to game functions that expect a time, such
as
DelayCommand()
,PlayAnimation()
, etc. - Game Time: a Time (either calendar Time or duration Time) with a minutes per hour setting of 60. This allows you to convert the time shown by the game clock into a time that matches how the characters in the game would perceive it. For example, with the default minutes per hour setting of 2, the Time "13:01" would correspond to a Game Time of "13:30".
- Normalizing Times: You can normalize a Time to ensure none of its units are overflowing their bounds. For example, a Time with a Minute field of 0 and Second field of 90 would be normalized to a Minute of 1 and a Second of 30. Normalizing a Time also causes all non-zero units to take the same sign (either positive or negative), so a Time with a Minute of 1 and a Second of -30 would normalize to a Second of 30. When normalizing a Time, you can also change the minutes per hour setting. This is how the functions in this file convert between Time and Game Time.
Note: For brevity, some functions have a Time
variant and a Duration
variant. In these cases, the Time
variant refers to a calendar Time (e.g.,
StringToTime()
converts to a calendar Time while StringToDuration()
refers
to a duration Time). If no Duration
variant of the function is present, the
function may refer to a calendar Time or a duration Time (e.g.,
TimeToString()
accepts both types).
The scripts in this section only require util_i_times.nss
.
You can create a calendar Time using GetTime()
and a duration Time with
GetDuration()
:
struct Time t = GetTime(1372, 6, 1, 13);
struct Time d = GetDuration(1372, 5, 0, 13);
You could also parse an ISO 8601 time string into a calendar Time or duration Time:
struct Time tTime = StringToTime("1372-06-01 13:00:00:000");
struct Time tDur = StringToDuration("1372-05-00 13:00:00:000");
// Negative durations are allowed:
struct Time tNeg = StringToDuration("-1372-05-00 13:00:00:000");
// Missing units are assumed to be their lowest bound:
struct Time a = StringToTime("1372-06-01 00:00:00:000");
struct Time b = StringToTime("1372-06-01");
struct Time c = StringToTime("1372-06");
Assert(a == b);
Assert(b == c);
You can also create a Time manually by declaring a new Time struct and setting the fields independently:
struct Time t;
t.Type = TIME_TYPE_CALENDAR;
t.Year = 1372;
t.Month = 6;
t.Day = 1;
t.Hour = 13;
// ...
When not using the GetTime()
function, it's a good idea to normalize the
resultant Time to distribute the field values correctly:
struct Time t = NewTime();
t.Second = 90;
t = NormalizeTime(t);
Assert(t.Minute == 1);
Assert(t.Second == 30);
// Assuming the default module setting of 2 minutes per hour
struct Time tTime = StringToTime("1372-06-01 13:01:00:000");
Assert(tTime.Hour == 13);
Assert(tTime.Minute == 1);
struct Time tGame = TimeToGameTime(tTime);
Assert(tGame.Hour == 13);
Assert(tGame.Minute == 30);
struct tBack = GameTimeToTime(tGame);
Assert(tTime == tBack);
struct Time tTime = GetCurrentTime();
struct Time tGame = GetCurrentGameTime();
Note: You can only set the time forward in NWN.
struct Time t = StringToTime("2022-08-25 13:00:00:000");
SetCurrentTime(t);
Alternatively, you can advance the current Time by a duration Time:
AdvanceCurrentTime(FloatToDuration(120.0));
You can reduce the precision of a Time. Units smaller than the precision limit will be at their lower bound:
struct Time a = GetTime(1372, 6, 1, 13);
struct Time b = GetTime(1372, 6, 1);
struct Time c = GetPrecisionTime(a, TIME_UNIT_DAY);
struct Time d = GetPrecisionTime(a, TIME_UNIT_MONTH);
Assert(a != b);
Assert(b == c);
Assert(b == d);
The easiest way to save a Time and get it later is to use the SetLocalTime()
and GetLocalTime()
functions. These functions convert a Time into json and
save it as a local variable.
In this example, we save the server start time OnModuleLoad and then get it later:
// OnModuleLoad
SetLocalTime(GetModule(), "ServerStart", GetCurrentTime());
// later on...
struct Time tServerStart = GetLocalTime(GetModule(), "ServerStart");
If you want to store a Time in a database, you can convert it into json or into a string before passing it to a query. The json method is preferable for persistent storage, since it is guaranteed to be correct if the module's minutes per hour setting changes after the value is stored:
struct Time tTime = GetCurrentTime();
json jTime = TimeToJson(tTime);
string sSql = "INSERT INTO data (varname, value) VALUES ('ServerTime', @time);";
sqlquery q = SqlPrepareQueryCampaign("mydb", sSql);
SqlBindJson(q, "@time", jTime);
SqlStep(q);
You can then convert the json back into a Time:
string Time tTime;
string sSql = "SELECT value FROM data WHERE varname='ServerTime';";
sqlquery q = SqlPrepareQueryCampaign("mydb", sSql);
if (SqlStep(q))
tTime = JsonToTime(SqlGetJson(q, 0));
For simpler applications (such as saving to the module's volatile database),
converting to a string works fine and could even be preferable since you can use
sqlite's <
, >
, and =
operators to check if one time is before, after, or
equal to another.
struct Time tTime = GetCurrentTime();
string sTime = TimeToString();
string sSql = "INSERT INTO data (varname, value) VALUES ('ServerTime', @time);";
sqlquery q = SqlPrepareQueryCampaign("mydb", sSql);
SqlBindString(q, "@time", sTime);
SqlStep(q);
To check if one time is before or after another:
struct Time a = StringToTime("1372-06-01 13:00:00:000");
struct Time b = StringToTime("1372-06-01 13:01:30:500");
Assert(GetIsTimeBefore(a, b));
Assert(!GetIsTimeAfter(a, b));
To check if two times are equal:
struct Time a = StringToTime("1372-06-01 13:00:00:000");
struct Time b = StringToTime("1372-06-01 13:01:00:000");
struct Time c = TimeToGameTime(b);
Assert(!GetIsTimeEqual(a, b));
Assert(GetIsTimeEqual(b, c));
// To check for exactly equal:
Assert(b != c);
To check the amount of time between two Times:
struct Time a = StringToTime("1372-06-01 13:00:00:000");
struct Time b = StringToTime("1372-06-01 13:01:30:500");
struct Time tDur = GetDurationBetween(a, b);
Assert(DurationToFloat(tDur) == 90.5);
To check if a duration has passed since a Time:
int CheckForMinRestTime(object oPC, float fMinTime)
{
struct Time tSince = GetDurationSince(GetLocalTime(oPC, "LastRest"));
return DurationToFloat(tSince) >= fMinTime;
}
To calculate the duration until a Time is reached:
struct Time tMidnight = GetTime(GetCalendarYear(), GetCalendarMonth(), GetCalendarDay() + 1);
struct Time tDurToMidnight = GetDurationUntil(tMidnight);
float fDurToMidnight = DurationToFloat(tDurToMidnight);
The Format*()
functions require util_i_strftime.nss
and
util_c_strftime.nss
.
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.
%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 keyLOCALE_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 keyLOCALE_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 keyLOCALE_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 keyLOCALE_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 keyLOCALE_DATETIME_FORMAT
for the%c
conversion specification andERA_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 ofsFormat
forFormatDateTime()
.%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 keyERA_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 ont.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 keyLOCALE_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 keyLOCALE_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 keyLOCALE_TIME_FORMAT
for the%x
conversion specification andERA_TIME_FORMAT
for the%Ex
conversion specification. With the default settings, this is equivalent to%Y-%m-%d
. This is the default value ofsFormat
forFomatDate()
.%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 keyLOCALE_DATE_FORMAT
for the%X
conversion specification andERA_DATE_FORMAT
for the%EX
conversion specification. With the default settings, this is equivalent to%H:%M:%S
. This is the default value ofsFormat
forFormatTime()
.%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 keyERA_FORMAT
; with the default era settings, this is equivalent to%Ey %EC
.)%%
: A literal%
character.
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
.
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.
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"
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:
json jLocale = JsonObject();
Alternatively, you can initialize a locale with the default values from
util_c_times.nss
:
json jLocale = NewLocale();
Keys are then added using SetLocaleString()
:
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:
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:
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:
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, theFormatTime()
function will useLOCALE_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, theFormatTime()
function will useLOCALE_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. Seeutil_c_times.nss
's documentation ofDEFAULT_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 toLOCALE_DATETIME_FORMAT
. Aliased by%Ec
.ERA_DATE_FORMAT
: a format string to display the date without the time. If not set, will fall back toLOCALE_DATE_FORMAT
. Aliased by%Ex
.ERA_TIME_FORMAT
: a format string to display the time without the date. If not set, will fall back toLOCALE_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
.
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:
// 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:
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()
:
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:
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
.