Parameterize with timezones

This commit is contained in:
Bruce Hill 2024-09-30 01:53:39 -04:00
parent 37780cb323
commit 793717729a
4 changed files with 197 additions and 66 deletions

View File

@ -1,13 +1,20 @@
# DateTime
Tomo has a builtin datatype for representing a specific single point in time:
`DateTime`. A DateTime object is internally represented using a UNIX epoch in
seconds and a number of nanoseconds to represent sub-second times (in C, the
`DateTime`. A DateTime object is internally represented using a UNIX timestamp
in seconds and a number of nanoseconds to represent sub-second times (in C, the
equivalent of `struct timeval`). DateTime values do not represent calendar
dates or clock times, they represent an exact moment in time, such as the
moment when a file was last modified on the filesystem or the current moment
(`DateTime.now()`).
⚠️⚠️⚠️ **WARNING** ⚠️⚠️⚠️ Dates and times are deeply counterintuitive and you should
be extremely cautious when writing code that deals with dates and times. Effort
has been made to ensure that Tomo's `DateTime` code uses standard libraries and
is as correct as possible, but counterintuitive behaviors around time zones,
daylight savings time, leap seconds, and other anomalous time situations can
still cause bugs if you're not extremely careful.
## Time Zones
Because humans are not able to easily understand UNIX timestamps, the default
@ -29,8 +36,12 @@ time. In that example, the initial time would be 3am March 1, 2023 in UTC, so
one month later would be 3am April 1, 2023 in UTC, which is which is 11am March
31st in local time. Most users would be unpleasantly surprised to find out that
when it's February 28th in local time, one month later is March 28th until 8pm,
at which point it becomes March 31st! For functions where this matters, there
is an extra `local_time` argument that is `yes` by default.
at which point it becomes March 31st!
For various functions where time zones matter, there is an optional `timezone`
argument that, if set, will override the timezone when performing calculations.
If unspecified, it is assumed that the current local timezone should be used.
Time zones are specified by name, such as `America/New_York` or `EDT`.
## DateTime Methods
@ -46,24 +57,25 @@ example, one year from January 1, 2024 is January 1, 2025, which is 366 days
later because 2024 is a leap year. Similarly, adding one month may add anywhere
from 28 to 31 days, depending on the starting month. Days and weeks are
affected by leap seconds. For this reason, `after()` takes an argument,
`local_time` which is used to determine whether time offsets should be
calculated using the current local time or UTC.
`timezone` which is used to determine in which timezone the offsets should be
calculated.
**Usage:**
```markdown
datetime:after(seconds : Num = 0.0, minutes : Num = 0.0, hours : Num = 0.0, days : Int = 0, weeks : Int = 0, months : Int = 0, years : Int = 0, local_time : Bool = yes) -> DateTime
datetime:after(seconds : Num = 0.0, minutes : Num = 0.0, hours : Num = 0.0, days : Int = 0, weeks : Int = 0, months : Int = 0, years : Int = 0, timezone : Text? = !Text) -> DateTime
```
**Parameters:**
- `seconds`: An amount of seconds to offset the datetime (default: 0).
- `minutes`: An amount of minutes to offset the datetime (default: 0).
- `hours`: An amount of hours to offset the datetime (default: 0).
- `days`: An amount of days to offset the datetime (default: 0).
- `weeks`: An amount of weeks to offset the datetime (default: 0).
- `months`: An amount of months to offset the datetime (default: 0).
- `years`: An amount of years to offset the datetime (default: 0).
- `local_time`: Whether to perform the calculations in local time (default: `yes`) or, if not, in UTC time.
- `seconds` (optional): An amount of seconds to offset the datetime (default: 0).
- `minutes` (optional): An amount of minutes to offset the datetime (default: 0).
- `hours` (optional): An amount of hours to offset the datetime (default: 0).
- `days` (optional): An amount of days to offset the datetime (default: 0).
- `weeks` (optional): An amount of weeks to offset the datetime (default: 0).
- `months` (optional): An amount of months to offset the datetime (default: 0).
- `years` (optional): An amount of years to offset the datetime (default: 0).
- `timezone` (optional): If specified, perform perform the calculations in the
given timezone. If unspecified, the current local timezone will be used.
**Returns:**
A new `DateTime` offset by the given amount.
@ -84,12 +96,12 @@ specifier, which gives the date in `YYYY-MM-DD` form.
**Usage:**
```markdown
datetime:date(local_time : Bool = yes) -> Text
datetime:date(timezone : Text? = !Text) -> Text
```
**Parameters:**
- `local_time`: Whether to use local time (default: `yes`) or UTC.
- `timezone` (optional): If specified, give the date in the given timezone (otherwise, use the current local timezone).
**Returns:**
The date in `YYYY-MM-DD` format.
@ -107,17 +119,18 @@ The date in `YYYY-MM-DD` format.
**Description:**
Using the C-style [`strftime`](https://linux.die.net/man/3/strftime) format
options, return a text representation of the given date in the given format. If
`local_time` is `no`, then use UTC instead of the current locale's timezone.
`timezone` is specified, use that timezone instead of the current local
timezone.
**Usage:**
```markdown
datetime:format(format: Text = "%Y-%m-%dT%H:%M:%S%z", local_time : Bool = yes) -> Text
datetime:format(format: Text = "%Y-%m-%dT%H:%M:%S%z", timezone : Text? = !Text) -> Text
```
**Parameters:**
- `format`: The `strftime` format to use (default: `"%Y-%m-%dT%H:%M:%S%z"`).
- `local_time`: Whether to use local time (default: `yes`) or UTC.
- `timezone` (optional): If specified, use the given timezone (otherwise, use the current local timezone).
**Returns:**
Nothing.
@ -165,7 +178,7 @@ provided optional fields.
**Usage:**
```markdown
datetime:get(year : &Int? = !&Int, month : &Int? = !&Int, day : &Int? = !&Int, hour : &Int? = !&Int, minute : &Int? = !&Int, second : &Int? = !&Int, nanosecond : &Int? = !&Int, weekday : &Int? = !&Int, local_time=yes) -> Void
datetime:get(year : &Int? = !&Int, month : &Int? = !&Int, day : &Int? = !&Int, hour : &Int? = !&Int, minute : &Int? = !&Int, second : &Int? = !&Int, nanosecond : &Int? = !&Int, weekday : &Int? = !&Int, timezone : Text? = !Text) -> Void
```
**Parameters:**
@ -178,7 +191,7 @@ datetime:get(year : &Int? = !&Int, month : &Int? = !&Int, day : &Int? = !&Int, h
- `second`: If non-null, store the second of the minute here (0-59).
- `nanosecond`: If non-null, store the nanosecond of the second here (0-1,000,000,000).
- `weekday`: If non-null, store the day of the week here (sunday=1, saturday=7)
- `local_time`: Whether to use the local timezone (default: `yes`) or UTC.
- `timezone` (optional): If specified, give values in the given timezone (otherwise, use the current local timezone).
**Returns:**
Nothing.
@ -194,6 +207,33 @@ dt:get(month=&month)
---
### `get_local_timezone`
**Description:**
Get the local timezone's name (e.g. `America/New_York` or `UTC`. By default,
this value is read from `/etc/localtime`, however, this can be overridden by
calling `DateTime.set_local_timezone(...)`.
**Usage:**
```markdown
DateTime.get_local_timezone() -> Text
```
**Parameters:**
None.
**Returns:**
The name of the current local timezone.
**Example:**
```markdown
>> DateTime.get_local_timezone()
= "America/New_York"
```
---
### `hours_till`
**Description:**
@ -353,13 +393,14 @@ between two `DateTime`s. For example: `5 minutes ago` or `1 day later`
**Usage:**
```markdown
datetime:relative(relative_to : DateTime = DateTime.now(), local_time : Bool = yes) -> Text
datetime:relative(relative_to : DateTime = DateTime.now(), timezone : Text? = !Text) -> Text
```
**Parameters:**
- `relative_to` (optional): The time against which the relative time is calculated (default: `DateTime.now()`).
- `local_time` (optional): Whether or not to perform calculations in local time (default: `yes`).
- `timezone` (optional): If specified, perform calculations in the given
timezone (otherwise, use the current local timezone).
**Returns:**
Return a plain English textual representation of the approximate time
@ -406,6 +447,36 @@ the_future := now():after(seconds=1)
---
### `set_local_timezone`
**Description:**
Set the current local timezone to a given value by name (e.g.
`America/New_York` or `UTC`). The local timezone is used as the default
timezone for performing calculations and constructing `DateTime` objects from
component parts. It's also used as the default way that `DateTime` objects are
converted to text.
**Usage:**
```markdown
DateTime.set_local_timezone(timezone : Text? = !Text) -> Void
```
**Parameters:**
- `timezone` (optional): if specified, set the current local timezone to the
timezone with the given name. If null, reset the current local timezone to
the system default (the value referenced in `/etc/localtime`).
**Returns:**
Nothing.
**Example:**
```markdown
DateTime.set_local_timezone("America/Los_Angeles")
```
---
### `time`
**Description:**
@ -413,14 +484,14 @@ Return a text representation of the time component of the given datetime.
**Usage:**
```markdown
datetime:time(seconds : Bool = no, am_pm : Bool = yes, local_time : Bool = yes) -> Text
datetime:time(seconds : Bool = no, am_pm : Bool = yes, timezone : Text? = !Text) -> Text
```
**Parameters:**
- `seconds`: Whether to include seconds in the time (default: `no`).
- `am_pm`: Whether to use am/pm in the representation or use a 24-hour clock (default: `yes`).
- `local_time`: Whether to use local time (default: `yes`) or UTC.
- `timezone` (optional): If specified, give the time in the given timezone (otherwise, use the current local timezone).
**Returns:**
A text representation of the time component of the datetime.

View File

@ -265,18 +265,20 @@ env_t *new_compilation_unit(CORD libname)
// Used as a default for functions below:
{"now", "DateTime$now", "func()->DateTime"},
{"after", "DateTime$after", "func(dt:DateTime,seconds=0.0,minutes=0.0,hours=0.0,days=0,weeks=0,months=0,years=0,local_time=yes)->DateTime"},
{"date", "DateTime$date", "func(dt:DateTime,local_time=yes)->Text"},
{"format", "DateTime$format", "func(dt:DateTime,format=\"%Y-%m-%dT%H:%M:%S%z\",local_time=yes)->Text"},
{"after", "DateTime$after", "func(dt:DateTime,seconds=0.0,minutes=0.0,hours=0.0,days=0,weeks=0,months=0,years=0,timezone=!Text)->DateTime"},
{"date", "DateTime$date", "func(dt:DateTime,timezone=!Text)->Text"},
{"format", "DateTime$format", "func(dt:DateTime,format=\"%Y-%m-%dT%H:%M:%S%z\",timezone=!Text)->Text"},
{"from_unix_timestamp", "DateTime$from_unix_timestamp", "func(timestamp:Int64)->DateTime"},
{"get", "DateTime$get", "func(dt:DateTime,year=!&Int,month=!&Int,day=!&Int,hour=!&Int,minute=!&Int,second=!&Int,nanosecond=!&Int,weekday=!&Int, local_time=yes)"},
{"get", "DateTime$get", "func(dt:DateTime,year=!&Int,month=!&Int,day=!&Int,hour=!&Int,minute=!&Int,second=!&Int,nanosecond=!&Int,weekday=!&Int, timezone=!Text)"},
{"get_local_timezone", "DateTime$get_local_timezone", "func()->Text"},
{"hours_till", "DateTime$hours_till", "func(now:DateTime,then:DateTime)->Num"},
{"minutes_till", "DateTime$minutes_till", "func(now:DateTime,then:DateTime)->Num"},
{"new", "DateTime$new", "func(year:Int,month:Int,day:Int,hour=0,minute=0,second=0.0)->DateTime"},
{"new", "DateTime$new", "func(year:Int,month:Int,day:Int,hour=0,minute=0,second=0.0,timezone=!Text)->DateTime"},
{"parse", "DateTime$parse", "func(text:Text, format=\"%Y-%m-%dT%H:%M:%S%z\")->DateTime?"},
{"relative", "DateTime$relative", "func(dt:DateTime,relative_to=DateTime.now(),local_time=yes)->Text"},
{"relative", "DateTime$relative", "func(dt:DateTime,relative_to=DateTime.now(),timezone=!Text)->Text"},
{"seconds_till", "DateTime$seconds_till", "func(now:DateTime,then:DateTime)->Num"},
{"time", "DateTime$time", "func(dt:DateTime,seconds=no,am_pm=yes,local_time=yes)->Text"},
{"set_local_timezone", "DateTime$set_local_timezone", "func(timezone=!Text)"},
{"time", "DateTime$time", "func(dt:DateTime,seconds=no,am_pm=yes,timezone=!Text)->Text"},
{"unix_timestamp", "DateTime$unix_timestamp", "func(dt:DateTime)->Int64"},
)},
{"Path", Type(TextType, .lang="Path", .env=namespace_env(env, "Path")), "Text_t", "Text$info", TypedArray(ns_entry_t,

View File

@ -4,14 +4,18 @@
#include <err.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>
#include "datatypes.h"
#include "datetime.h"
#include "optionals.h"
#include "patterns.h"
#include "stdlib.h"
#include "text.h"
#include "util.h"
static OptionalText_t _local_timezone = NULL_TEXT;
public Text_t DateTime$as_text(const DateTime_t *dt, bool colorize, const TypeInfo *type)
{
(void)type;
@ -44,8 +48,10 @@ public DateTime_t DateTime$now(void)
return (DateTime_t){.tv_sec=ts.tv_sec, .tv_usec=ts.tv_nsec};
}
public DateTime_t DateTime$new(Int_t year, Int_t month, Int_t day, Int_t hour, Int_t minute, double second)
public DateTime_t DateTime$new(Int_t year, Int_t month, Int_t day, Int_t hour, Int_t minute, double second, OptionalText_t timezone)
{
if (timezone.length >= 0)
DateTime$set_local_timezone(timezone);
struct tm info = {
.tm_min=Int_to_Int32(minute, false),
.tm_hour=Int_to_Int32(hour, false),
@ -55,19 +61,26 @@ public DateTime_t DateTime$new(Int_t year, Int_t month, Int_t day, Int_t hour, I
.tm_isdst=-1,
};
time_t t = mktime(&info);
if (timezone.length >= 0)
DateTime$set_local_timezone(_local_timezone);
return (DateTime_t){.tv_sec=t + (time_t)second, .tv_usec=(suseconds_t)(fmod(second, 1.0) * 1e9)};
}
public DateTime_t DateTime$after(DateTime_t dt, double seconds, double minutes, double hours, Int_t days, Int_t weeks, Int_t months, Int_t years, bool local_time)
public DateTime_t DateTime$after(DateTime_t dt, double seconds, double minutes, double hours, Int_t days, Int_t weeks, Int_t months, Int_t years, OptionalText_t timezone)
{
double offset = seconds + 60.*minutes + 3600.*hours;
dt.tv_sec += (time_t)offset;
struct tm info = {};
if (local_time)
if (timezone.length >= 0) {
OptionalText_t old_timezone = _local_timezone;
DateTime$set_local_timezone(timezone);
localtime_r(&dt.tv_sec, &info);
else
gmtime_r(&dt.tv_sec, &info);
DateTime$set_local_timezone(old_timezone);
} else {
localtime_r(&dt.tv_sec, &info);
}
info.tm_mday += Int_to_Int32(days, false) + 7*Int_to_Int32(weeks, false);
info.tm_mon += Int_to_Int32(months, false);
info.tm_year += Int_to_Int32(years, false);
@ -96,13 +109,18 @@ CONSTFUNC public double DateTime$hours_till(DateTime_t now, DateTime_t then)
public void DateTime$get(
DateTime_t dt, Int_t *year, Int_t *month, Int_t *day, Int_t *hour, Int_t *minute, Int_t *second,
Int_t *nanosecond, Int_t *weekday, bool local_time)
Int_t *nanosecond, Int_t *weekday, OptionalText_t timezone)
{
struct tm info = {};
if (local_time)
if (timezone.length >= 0) {
OptionalText_t old_timezone = _local_timezone;
DateTime$set_local_timezone(timezone);
localtime_r(&dt.tv_sec, &info);
else
gmtime_r(&dt.tv_sec, &info);
DateTime$set_local_timezone(old_timezone);
} else {
localtime_r(&dt.tv_sec, &info);
}
if (year) *year = I(info.tm_year + 1900);
if (month) *month = I(info.tm_mon + 1);
@ -114,27 +132,34 @@ public void DateTime$get(
if (weekday) *weekday = I(info.tm_wday + 1);
}
public Text_t DateTime$format(DateTime_t dt, Text_t fmt, bool local_time)
public Text_t DateTime$format(DateTime_t dt, Text_t fmt, OptionalText_t timezone)
{
struct tm info;
struct tm *final_info = local_time ? localtime_r(&dt.tv_sec, &info) : gmtime_r(&dt.tv_sec, &info);
if (timezone.length >= 0) {
OptionalText_t old_timezone = _local_timezone;
DateTime$set_local_timezone(timezone);
localtime_r(&dt.tv_sec, &info);
DateTime$set_local_timezone(old_timezone);
} else {
localtime_r(&dt.tv_sec, &info);
}
static char buf[256];
size_t len = strftime(buf, sizeof(buf), Text$as_c_string(fmt), final_info);
size_t len = strftime(buf, sizeof(buf), Text$as_c_string(fmt), &info);
return Text$format("%.*s", (int)len, buf);
}
public Text_t DateTime$date(DateTime_t dt, bool local_time)
public Text_t DateTime$date(DateTime_t dt, OptionalText_t timezone)
{
return DateTime$format(dt, Text("%F"), local_time);
return DateTime$format(dt, Text("%F"), timezone);
}
public Text_t DateTime$time(DateTime_t dt, bool seconds, bool am_pm, bool local_time)
public Text_t DateTime$time(DateTime_t dt, bool seconds, bool am_pm, OptionalText_t timezone)
{
Text_t text;
if (seconds)
text = DateTime$format(dt, am_pm ? Text("%l:%M:%S%P") : Text("%T"), local_time);
text = DateTime$format(dt, am_pm ? Text("%l:%M:%S%P") : Text("%T"), timezone);
else
text = DateTime$format(dt, am_pm ? Text("%l:%M%P") : Text("%H:%M"), local_time);
text = DateTime$format(dt, am_pm ? Text("%l:%M%P") : Text("%H:%M"), timezone);
return Text$trim(text, Pattern(" "), true, true);
}
@ -162,19 +187,21 @@ static inline Text_t num_format(long n, const char *unit)
return Text$format((n == 1 || n == -1) ? "%ld %s %s" : "%ld %ss %s", n < 0 ? -n : n, unit, n < 0 ? "ago" : "later");
}
public Text_t DateTime$relative(DateTime_t dt, DateTime_t relative_to, bool local_time)
public Text_t DateTime$relative(DateTime_t dt, DateTime_t relative_to, OptionalText_t timezone)
{
struct tm info = {};
if (local_time)
localtime_r(&dt.tv_sec, &info);
else
gmtime_r(&dt.tv_sec, &info);
struct tm relative_info = {};
if (local_time)
if (timezone.length >= 0) {
OptionalText_t old_timezone = _local_timezone;
DateTime$set_local_timezone(timezone);
localtime_r(&dt.tv_sec, &info);
localtime_r(&relative_to.tv_sec, &relative_info);
else
gmtime_r(&relative_to.tv_sec, &relative_info);
DateTime$set_local_timezone(old_timezone);
} else {
localtime_r(&dt.tv_sec, &info);
localtime_r(&relative_to.tv_sec, &relative_info);
}
double second_diff = DateTime$seconds_till(relative_to, dt);
if (info.tm_year != relative_info.tm_year && fabs(second_diff) > 365.*24.*60.*60.)
@ -209,6 +236,34 @@ CONSTFUNC public DateTime_t DateTime$from_unix_timestamp(Int64_t timestamp)
return (DateTime_t){.tv_sec=(time_t)timestamp};
}
public void DateTime$set_local_timezone(OptionalText_t timezone)
{
if (timezone.length >= 0) {
setenv("TZ", Text$as_c_string(timezone), 1);
} else {
unsetenv("TZ");
}
_local_timezone = timezone;
tzset();
}
public Text_t DateTime$get_local_timezone(void)
{
if (_local_timezone.length < 0) {
static char buf[PATH_MAX];
ssize_t len = readlink("/etc/localtime", buf, sizeof(buf));
if (len < 0)
fail("Could not get local timezone!");
char *zoneinfo = strstr(buf, "/zoneinfo/");
if (zoneinfo)
_local_timezone = Text$from_str(zoneinfo + strlen("/zoneinfo/"));
else
fail("Could not resolve local timezone!");
}
return _local_timezone;
}
public const TypeInfo DateTime$info = {
.size=sizeof(DateTime_t),
.align=__alignof__(DateTime_t),

View File

@ -6,25 +6,28 @@
#include "datatypes.h"
#include "integers.h"
#include "optionals.h"
#include "types.h"
#include "util.h"
Text_t DateTime$as_text(const DateTime_t *dt, bool colorize, const TypeInfo *type);
PUREFUNC int32_t DateTime$compare(const DateTime_t *a, const DateTime_t *b, const TypeInfo *type);
DateTime_t DateTime$now(void);
DateTime_t DateTime$new(Int_t year, Int_t month, Int_t day, Int_t hour, Int_t minute, double second);
DateTime_t DateTime$after(DateTime_t dt, double seconds, double minutes, double hours, Int_t days, Int_t weeks, Int_t months, Int_t years, bool local_time);
DateTime_t DateTime$new(Int_t year, Int_t month, Int_t day, Int_t hour, Int_t minute, double second, OptionalText_t timezone);
DateTime_t DateTime$after(DateTime_t dt, double seconds, double minutes, double hours, Int_t days, Int_t weeks, Int_t months, Int_t years, OptionalText_t timezone);
CONSTFUNC double DateTime$seconds_till(DateTime_t now, DateTime_t then);
CONSTFUNC double DateTime$minutes_till(DateTime_t now, DateTime_t then);
CONSTFUNC double DateTime$hours_till(DateTime_t now, DateTime_t then);
void DateTime$get(DateTime_t dt, Int_t *year, Int_t *month, Int_t *day, Int_t *hour, Int_t *minute, Int_t *second, Int_t *nanosecond, Int_t *weekday, bool local_time);
Text_t DateTime$format(DateTime_t dt, Text_t fmt, bool local_time);
Text_t DateTime$date(DateTime_t dt, bool local_time);
Text_t DateTime$time(DateTime_t dt, bool seconds, bool am_pm, bool local_time);
void DateTime$get(DateTime_t dt, Int_t *year, Int_t *month, Int_t *day, Int_t *hour, Int_t *minute, Int_t *second, Int_t *nanosecond, Int_t *weekday, OptionalText_t timezone);
Text_t DateTime$format(DateTime_t dt, Text_t fmt, OptionalText_t timezone);
Text_t DateTime$date(DateTime_t dt, OptionalText_t timezone);
Text_t DateTime$time(DateTime_t dt, bool seconds, bool am_pm, OptionalText_t timezone);
OptionalDateTime_t DateTime$parse(Text_t text, Text_t format);
Text_t DateTime$relative(DateTime_t dt, DateTime_t relative_to, bool local_time);
Text_t DateTime$relative(DateTime_t dt, DateTime_t relative_to, OptionalText_t timezone);
CONSTFUNC Int64_t DateTime$unix_timestamp(DateTime_t dt);
CONSTFUNC DateTime_t DateTime$from_unix_timestamp(Int64_t timestamp);
void DateTime$set_local_timezone(OptionalText_t timezone);
Text_t DateTime$get_local_timezone(void);
extern const TypeInfo DateTime$info;