From 4231789b71bb42c4ab04e125f98fe5eb3cf030b6 Mon Sep 17 00:00:00 2001 From: Bruce Hill Date: Sun, 17 Nov 2024 14:49:03 -0500 Subject: Rename datetime -> moment --- Makefile | 2 +- ast.h | 6 +- compile.c | 22 +- docs/README.md | 8 +- docs/datetime.md | 732 ---------------------------------------------------- docs/moments.md | 733 +++++++++++++++++++++++++++++++++++++++++++++++++++++ environment.c | 62 ++--- parse.c | 20 +- stdlib/datatypes.h | 4 +- stdlib/datetime.c | 317 ----------------------- stdlib/datetime.h | 43 ---- stdlib/moments.c | 317 +++++++++++++++++++++++ stdlib/moments.h | 43 ++++ stdlib/optionals.c | 6 +- stdlib/optionals.h | 2 +- stdlib/paths.c | 18 +- stdlib/paths.h | 6 +- stdlib/tomo.h | 2 +- test/datetime.tm | 49 ---- test/moments.tm | 49 ++++ typecheck.c | 4 +- types.c | 6 +- types.h | 4 +- 23 files changed, 1228 insertions(+), 1227 deletions(-) delete mode 100644 docs/datetime.md create mode 100644 docs/moments.md delete mode 100644 stdlib/datetime.c delete mode 100644 stdlib/datetime.h create mode 100644 stdlib/moments.c create mode 100644 stdlib/moments.h delete mode 100644 test/datetime.tm create mode 100644 test/moments.tm diff --git a/Makefile b/Makefile index 936ea0a3..9fdc0581 100644 --- a/Makefile +++ b/Makefile @@ -32,7 +32,7 @@ LDLIBS=-lgc -lcord -lm -lunistring -lgmp -ldl BUILTIN_OBJS=stdlib/siphash.o stdlib/arrays.o stdlib/bools.o stdlib/bytes.o stdlib/channels.o stdlib/nums.o stdlib/integers.o \ stdlib/pointers.o stdlib/memory.o stdlib/text.o stdlib/threads.o stdlib/c_strings.o stdlib/tables.o \ stdlib/types.o stdlib/util.o stdlib/files.o stdlib/ranges.o stdlib/shell.o stdlib/paths.o stdlib/rng.o \ - stdlib/optionals.o stdlib/patterns.o stdlib/metamethods.o stdlib/functiontype.o stdlib/stdlib.o stdlib/datetime.o + stdlib/optionals.o stdlib/patterns.o stdlib/metamethods.o stdlib/functiontype.o stdlib/stdlib.o stdlib/moments.o TESTS=$(patsubst %.tm,%.tm.testresult,$(wildcard test/*.tm)) all: libtomo.so tomo diff --git a/ast.h b/ast.h index 3911f2ae..3535734c 100644 --- a/ast.h +++ b/ast.h @@ -141,7 +141,7 @@ typedef enum { Extern, StructDef, EnumDef, LangDef, Index, FieldAccess, Optional, NonOptional, - DateTime, + Moment, DocTest, Use, InlineCCode, @@ -309,8 +309,8 @@ struct ast_s { ast_t *value; } Optional, NonOptional; struct { - DateTime_t dt; - } DateTime; + Moment_t moment; + } Moment; struct { ast_t *expr; const char *output; diff --git a/compile.c b/compile.c index 404694ef..d380eaf3 100644 --- a/compile.c +++ b/compile.c @@ -227,7 +227,7 @@ CORD compile_type(type_t *t) case BoolType: return "Bool_t"; case ByteType: return "Byte_t"; case CStringType: return "char*"; - case DateTimeType: return "DateTime_t"; + case MomentType: return "Moment_t"; case BigIntType: return "Int_t"; case IntType: return CORD_asprintf("Int%ld_t", Match(t, IntType)->bits); case NumType: return Match(t, NumType)->bits == TYPE_NBITS64 ? "Num_t" : CORD_asprintf("Num%ld_t", Match(t, NumType)->bits); @@ -276,7 +276,7 @@ CORD compile_type(type_t *t) case TextType: return Match(nonnull, TextType)->lang ? compile_type(nonnull) : "OptionalText_t"; case IntType: case BigIntType: case NumType: case BoolType: case ByteType: - case ArrayType: case TableType: case SetType: case DateTimeType: + case ArrayType: case TableType: case SetType: case MomentType: return CORD_all("Optional", compile_type(nonnull)); case StructType: { if (nonnull == THREAD_TYPE) @@ -414,7 +414,7 @@ static CORD check_null(type_t *t, CORD value) return CORD_all("(", value, ").is_null"); else if (t->tag == EnumType) return CORD_all("((", value, ").tag == 0)"); - else if (t->tag == DateTimeType) + else if (t->tag == MomentType) return CORD_all("((", value, ").tv_usec < 0)"); errx(1, "Optional check not implemented for: %T", t); } @@ -1511,7 +1511,7 @@ CORD expr_as_text(env_t *env, CORD expr, type_t *t, CORD color) // NOTE: this cannot use stack(), since bools may actually be bit fields: return CORD_asprintf("Bool$as_text((Bool_t[1]){%r}, %r, &Bool$info)", expr, color); case CStringType: return CORD_asprintf("CString$as_text(stack(%r), %r, &CString$info)", expr, color); - case DateTimeType: return CORD_asprintf("DateTime$as_text(stack(%r), %r, &DateTime$info)", expr, color); + case MomentType: return CORD_asprintf("Moment$as_text(stack(%r), %r, &Moment$info)", expr, color); case BigIntType: case IntType: case ByteType: case NumType: { CORD name = type_to_cord(t); return CORD_asprintf("%r$as_text(stack(%r), %r, &%r$info)", name, expr, color, name); @@ -1906,7 +1906,7 @@ CORD compile_null(type_t *t) case ChannelType: return "NULL"; case TextType: return "NULL_TEXT"; case CStringType: return "NULL"; - case DateTimeType: return "NULL_DATETIME"; + case MomentType: return "NULL_MOMENT"; case PointerType: return CORD_all("((", compile_type(t), ")NULL)"); case ClosureType: return "NULL_CLOSURE"; case NumType: return "nan(\"null\")"; @@ -1960,9 +1960,9 @@ CORD compile(env_t *env, ast_t *ast) return compile_null(t); } case Bool: return Match(ast, Bool)->b ? "yes" : "no"; - case DateTime: { - auto dt = Match(ast, DateTime)->dt; - return CORD_asprintf("((DateTime_t){.tv_sec=%ld, .tv_usec=%ld})", dt.tv_sec, dt.tv_usec); + case Moment: { + auto moment = Match(ast, Moment)->moment; + return CORD_asprintf("((Moment_t){.tv_sec=%ld, .tv_usec=%ld})", moment.tv_sec, moment.tv_usec); } case Var: { binding_t *b = get_binding(env, Match(ast, Var)->name); @@ -3114,8 +3114,8 @@ CORD compile(env_t *env, ast_t *ast) return compile_string_literal(Match(Match(call->args->value, TextJoin)->children->ast, TextLiteral)->cord); type_t *actual = get_type(env, call->args->value); return CORD_all("Text$as_c_string(", expr_as_text(env, compile(env, call->args->value), actual, "no"), ")"); - } else if (t->tag == DateTimeType) { - // DateTime constructor: + } else if (t->tag == MomentType) { + // Moment constructor: binding_t *new_binding = get_binding(Match(fn_t, TypeInfoType)->env, "new"); CORD arg_code = compile_arguments(env, ast, Match(new_binding->type, FunctionType)->args, call->args); return CORD_all(new_binding->code, "(", arg_code, ")"); @@ -3590,7 +3590,7 @@ CORD compile_type_info(env_t *env, type_t *t) else if (t == RNG_TYPE) return "&RNG$info"; switch (t->tag) { - case BoolType: case ByteType: case IntType: case BigIntType: case NumType: case CStringType: case DateTimeType: + case BoolType: case ByteType: case IntType: case BigIntType: case NumType: case CStringType: case MomentType: return CORD_all("&", type_to_cord(t), "$info"); case TextType: { auto text = Match(t, TextType); diff --git a/docs/README.md b/docs/README.md index 6d3567df..960ed4af 100644 --- a/docs/README.md +++ b/docs/README.md @@ -22,7 +22,7 @@ Information about Tomo's built-in types can be found here: - [Booleans](booleans.md) - [Bytes](bytes.md) - [Channels](channels.md) -- [DateTime](datetime.md) +- [Moment](moments.md) - [Enums](enums.md) - [Floating point numbers](nums.md) - [Integer Ranges](ranges.md) @@ -177,11 +177,11 @@ fail("Oh no!") ### `now` **Description:** -Gets the current time. This is an alias for `DateTime.now()`. +Gets the current time. This is an alias for `Moment.now()`. **Signature:** ```tomo -func now(->DateTime) +func now(->Moment) ``` **Parameters:** @@ -189,7 +189,7 @@ func now(->DateTime) None. **Returns:** -The current moment as a DateTime. +The current moment as a Moment. **Example:** ```tomo diff --git a/docs/datetime.md b/docs/datetime.md deleted file mode 100644 index 334c615b..00000000 --- a/docs/datetime.md +++ /dev/null @@ -1,732 +0,0 @@ -# DateTime - -Tomo has a builtin datatype for representing a specific single point in time: -`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. - -## Syntax - -DateTime literals can be specified using [ISO -8601](https://en.wikipedia.org/wiki/ISO_8601) syntax with an optional -square-bracket delimited time zone name afterwards. A space may be used instead -of a `T` in the ISO 8601 format for readability, and spaces may come before the -timezone. - -```tomo -2024-09-30 -2024-09-30T13:57 -2024-09-30 13:57 -2024-09-30 13:57:01 -2024-09-30 13:57:01 +04:00 -2024-09-30 13:57:01 [America/New_York] -``` - -## Time Zones - -Because humans are not able to easily understand UNIX timestamps, the default -textual representation of `DateTime` objects uses the current locale's -preferred representation of the DateTime in the current time zone: - -```tomo ->> DateTime.now() -= Sun Sep 29 18:20:12 2024 EDT -``` - -For various methods, it is assumed by default that users wish to perform -calculations and specify datetimes using the local time zone and daylight -savings time rules. For example, if a program is running in New York and it is -currently 11pm on February 28th, 2023 (the last day of the month) in local -time, it is assumed that "one month from now" refers to 11pm on March 28th, -2024 in local time, rather than referring to one month from the current UTC -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 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 `UTC`. - -## DateTime Methods - -### `after` - -**Description:** -Returns a DateTime that occurs after the specified time differences. Time -differences may be either positive or negative. - -**Note:** time offsets for days, months, weeks, and years do not refer to fixed -time intervals, but are relative to which date they are applied to. For -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, -`timezone` which is used to determine in which timezone the offsets should be -calculated. - -**Signature:** -```tomo -func after(datetime: DateTime, 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:** - -- `datetime`: The datetime used as a starting point. -- `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. - -**Example:** -```tomo ->> DateTime(2024, 9, 29, hour=19):after(days=1, minutes=30) -= Mon Sep 30 19:30:00 2024 EDT -``` - ---- - -### `date` - -**Description:** -Return a text representation of the datetime using the `"%F"` format -specifier, which gives the date in `YYYY-MM-DD` form. - -**Signature:** -```tomo -func date(datetime: DateTime, timezone : Text? = !Text -> Text) -``` - -**Parameters:** - -- `datetime`: The datetime to get the date from. -- `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. - -**Example:** -```tomo ->> DateTime(2024, 9, 29):date() -= "2024-09-29" -``` - ---- - -### `day_of_month` - -**Description:** -Return the integer day of the month (1-31). - -**Signature:** -```tomo -func day_of_month(datetime: DateTime, timezone : Text? = !Text -> Int) -``` - -**Parameters:** - -- `datetime`: The datetime to get the day of the month from. -- `timezone` (optional): If specified, use the given timezone (otherwise, use the current local timezone). - -**Returns:** -The day of the month as an integer (1-31). - -**Example:** -```tomo ->> DateTime(2024, 9, 29):day_of_month() -= 29 -``` - ---- - -### `day_of_week` - -**Description:** -Return the integer day of the week (1-7), where 1 = Sunday, 2 = Monday, -3 = Tuesday, 4 = Wednesday, 5 = Thursday, 6 = Friday, 7 = Saturday. - -**Signature:** -```tomo -func day_of_week(datetime: DateTime, timezone : Text? = !Text -> Int) -``` - -**Parameters:** - -- `datetime`: The datetime to get the day of the week from. -- `timezone` (optional): If specified, use the given timezone (otherwise, use the current local timezone). - -**Returns:** -The day of the week as an integer (1-7). - -**Example:** -```tomo ->> DateTime(2024, 9, 29):day_of_week() -= 1 -``` - ---- - -### `day_of_year` - -**Description:** -Return the integer day of the year (1-366, including leap years). - -**Signature:** -```tomo -func day_of_year(datetime: DateTime, timezone : Text? = !Text -> Int) -``` - -**Parameters:** - -- `datetime`: The datetime to get the day of the year from. -- `timezone` (optional): If specified, use the given timezone (otherwise, use the current local timezone). - -**Returns:** -The day of the year as an integer (1-366). - -**Example:** -```tomo ->> DateTime(2024, 9, 29):day_of_year() -= 272 -``` - ---- - -### `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 -`timezone` is specified, use that timezone instead of the current local -timezone. - -**Signature:** -```tomo -func format(datetime: DateTime, format: Text = "%Y-%m-%dT%H:%M:%S%z", timezone : Text? = !Text -> Text) -``` - -**Parameters:** - -- `datetime`: The datetime to format. -- `format`: The `strftime` format to use (default: `"%Y-%m-%dT%H:%M:%S%z"`). -- `timezone` (optional): If specified, use the given timezone (otherwise, use the current local timezone). - -**Returns:** -Nothing. - -**Example:** -```tomo ->> DateTime(2024, 9, 29):format("%A") -= "Sunday" -``` - ---- - -### `from_unix_timestamp` - -**Description:** -Return a datetime object that represents the same moment in time as -the given UNIX epoch timestamp (seconds since January 1, 1970 UTC). - -**Signature:** -```tomo -func from_unix_timestamp(timestamp: Int64 -> DateTime) -``` - -**Parameters:** - -- `timestamp`: The UNIX timestamp. - -**Returns:** -A `DateTime` object representing the same moment as the given UNIX timestamp. - -**Example:** -```tomo -# In the New York timezone: ->> DateTime.from_unix_timestamp(0) -= Wed Dec 31 19:00:00 1969 -``` - -### `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(...)`. - -**Signature:** -```tomo -func get_local_timezone(->Text) -``` - -**Parameters:** - -None. - -**Returns:** -The name of the current local timezone. - -**Example:** -```tomo ->> DateTime.get_local_timezone() -= "America/New_York" -``` - ---- - -### `hour` - -**Description:** -Return the hour of the day as an integer (1-24). - -**Signature:** -```tomo -func hour(datetime: DateTime, timezone : Text? = !Text -> Int) -``` - -**Parameters:** - -- `datetime`: The datetime to get the hour from. -- `timezone` (optional): If specified, use the given timezone (otherwise, use the current local timezone). - -**Returns:** -The hour of the day as an integer (1-24). - -**Example:** -```tomo ->> DateTime(2024, 9, 29, 11, 59):hour() -= 11 -``` - ---- - -### `hours_till` - -**Description:** -Return the number of hours until a given datetime. - -**Signature:** -```tomo -func hours_till(datetime: DateTime, then:DateTime -> Num) -``` - -**Parameters:** - -- `datetime`: The starting point datetime. -- `then`: Another datetime that we want to calculate the time offset from (in hours). - -**Returns:** -The number of hours (possibly fractional, possibly negative) until the given time. - -**Example:** -```tomo -the_future := now():after(hours=1, minutes=30) ->> now():hours_till(the_future) -= 1.5 -``` - ---- - -### `minute` - -**Description:** -Return the minute of the day as an integer (0-59). - -**Signature:** -```tomo -func minute(datetime: DateTime, timezone : Text? = !Text -> Int) -``` - -**Parameters:** - -- `datetime`: The datetime to get the minute from. -- `timezone` (optional): If specified, use the given timezone (otherwise, use the current local timezone). - -**Returns:** -The minute of the hour as an integer (0-59). - -**Example:** -```tomo ->> DateTime(2024, 9, 29, 11, 59):minute() -= 59 -``` - ---- - -### `minutes_till` - -**Description:** -Return the number of minutes until a given datetime. - -**Signature:** -```tomo -func minutes_till(datetime: DateTime, then:DateTime -> Num) -``` - -**Parameters:** - -- `datetime`: The starting point datetime. -- `then`: Another datetime that we want to calculate the time offset from (in minutes). - -**Returns:** -The number of minutes (possibly fractional, possibly negative) until the given time. - -**Example:** -```tomo -the_future := now():after(minutes=1, seconds=30) ->> now():minutes_till(the_future) -= 1.5 -``` - ---- - -### `month` - -**Description:** -Return the month of the year as an integer (1-12). - -**Signature:** -```tomo -func month(datetime: DateTime, timezone : Text? = !Text -> Int) -``` - -**Parameters:** - -- `datetime`: The datetime to get the month from. -- `timezone` (optional): If specified, use the given timezone (otherwise, use the current local timezone). - -**Returns:** -The month of the year as an integer (1-12). - -**Example:** -```tomo ->> DateTime(2024, 9, 29, 11, 59):month() -= 9 -``` - ---- - -### `nanosecond` - -**Description:** -Return the nanosecond of the second as an integer (0-999,999,999). - -**Signature:** -```tomo -func nanosecond(datetime: DateTime, timezone : Text? = !Text -> Int) -``` - -**Parameters:** - -- `datetime`: The datetime to get the nanosecond from. -- `timezone` (optional): If specified, use the given timezone (otherwise, use the current local timezone). - -**Returns:** -The nanosecond of the second as an integer (0-999,999,999). - -**Example:** -```tomo ->> DateTime(2024, 9, 29, 11, 59):month() -= 9 -``` - ---- - -### `new` - -**Description:** -Return a new `DateTime` object representing the given time parameters expressed -in local time. This function is the same as calling `DateTime` directly as a -constructor. - -**Signature:** -```tomo -func new(year : Int, month : Int, day : Int, hour : Int = 0, minute : Int = 0, second : Num = 0.0 -> DateTime) -``` - -**Parameters:** - -- `year`: The year. -- `month`: The month of the year (1-12). -- `day`: The day of the month (1-31). -- `hour`: The hour of the day (0-23) (default: 0). -- `minute`: The minute of the hour (0-59) (default: 0). -- `second`: The second of the minute (0-59) (default: 0.0). - -**Returns:** -A `DateTime` representing the given information in local time. If the given -parameters exceed reasonable bounds, the time values will wrap around. For -example, `DateTime.new(..., hour=3, minute=65)` is the same as -`DateTime.new(..., hour=4, minute=5)`. If any arguments cannot fit in a 32-bit -integer, an error will be raised. - -**Example:** -```tomo ->> DateTime.new(2024, 9, 29) -= Mon Sep 30 00:00:00 2024 EDT - -# March 1642, 2020: ->> DateTime(2020, 4, 1643) -= Sat Sep 28 00:00:00 2024 EDT -``` - ---- - -### `now` - -**Description:** -Get a `DateTime` object representing the current date and time. This function -is the same as the global function `now()`. - -**Signature:** -```tomo -func now(->DateTime) -``` - -**Parameters:** - -None. - -**Returns:** -Returns a `DateTime` object representing the current date and time. - -**Example:** -```tomo ->> DateTime.now() -= Sun Sep 29 20:22:48 2024 EDT -``` - ---- - -### `parse` - -**Description:** -Return a new `DateTime` object parsed from the given string in the given format, -or a null value if the value could not be successfully parsed. - -**Signature:** -```tomo -func parse(text: Text, format: Text = "%Y-%m-%dT%H:%M:%S%z" -> DateTime?) -``` - -**Parameters:** - -- `text`: The text to parse. -- `format`: The date format of the text being parsed (see: - [strptime](https://linux.die.net/man/3/strptime) for more info on this - format) (default: `"%Y-%m-%dT%H:%M:%S%z"`). - -**Returns:** -If the text was successfully parsed according to the given format, return a -`DateTime` representing that information. Otherwise, return a null value. - -**Example:** -```tomo ->> DateTime.parse("2024-09-29", "%Y-%m-%d")! -= Sun Sep 29 00:00:00 2024 EDT - ->> DateTime.parse("???", "%Y-%m-%d") -= !DateTime -``` - ---- - -### `relative` - -**Description:** -Return a plain English textual representation of the approximate time difference -between two `DateTime`s. For example: `5 minutes ago` or `1 day later` - -**Signature:** -```tomo -func relative(datetime: DateTime, relative_to : DateTime = DateTime.now(), timezone : Text? = !Text -> Text) -``` - -**Parameters:** - -- `datetime`: The datetime whose relative time you're getting. -- `relative_to` (optional): The time against which the relative time is calculated (default: `DateTime.now()`). -- `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 -difference between two `DateTime`s. For example: `5 minutes ago` or `1 day -later`. Return values are approximate and use only one significant unit of -measure with one significant digit, so a difference of 1.6 days will be -represented as `2 days later`. Datetimes in the past will have the suffix `" -ago"`, while datetimes in the future will have the suffix `" later"`. - -**Example:** -```tomo ->> now():after(days=2):relative() -= "2 days later" - ->> now():after(minutes=-65):relative() -= "1 hour ago" -``` - ---- - -### `second` - -**Description:** -Return the second of the minute as an integer (0-59). - -**Signature:** -```tomo -func second(datetime: DateTime, timezone : Text? = !Text -> Int) -``` - -**Parameters:** - -- `datetime`: The datetime to get the second from. -- `timezone` (optional): If specified, use the given timezone (otherwise, use the current local timezone). - -**Returns:** -The second of the hour as an integer (0-59). - -**Example:** -```tomo ->> DateTime(2024, 9, 29, 11, 30, 59):second() -= 59 -``` - ---- - -### `seconds_till` - -**Description:** -Return the number of seconds until a given datetime. - -**Signature:** -```tomo -func seconds_till(datetime: DateTime, then:DateTime -> Num) -``` - -**Parameters:** - -- `datetime`: The starting point datetime. -- `then`: Another datetime that we want to calculate the time offset from (in seconds). - -**Returns:** -The number of seconds (possibly fractional, possibly negative) until the given time. - -**Example:** -```tomo -the_future := now():after(seconds=1) ->> now():seconds_till(the_future) -= 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. - -**Signature:** -```tomo -func 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:** -```tomo -DateTime.set_local_timezone("America/Los_Angeles") -``` - ---- - -### `time` - -**Description:** -Return a text representation of the time component of the given datetime. - -**Signature:** -```tomo -func time(datetime: DateTime, seconds : Bool = no, am_pm : Bool = yes, timezone : Text? = !Text -> Text) -``` - -**Parameters:** - -- `datetime`: The datetime whose time value you want to get. -- `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`). -- `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. - -**Example:** -```tomo -dt := DateTime(2024, 9, 29, hours=13, minutes=59, seconds=30) - ->> dt:time() -= "1:59pm" - ->> dt:time(am_pm=no) -= "13:59" - ->> dt:time(seconds=yes) -= "1:59:30pm" -``` - ---- - -### `unix_timestamp` - -**Description:** -Get the UNIX timestamp of the given datetime (seconds since the UNIX epoch: -January 1, 1970 UTC). - -**Signature:** -```tomo -func unix_timestamp(datetime:DateTime->Int64) -``` - -**Parameters:** - -`datetime`: The datetime whose UNIX timestamp you want to get. - -**Returns:** -A 64-bit integer representation of the UNIX timestamp. - -**Example:** -```tomo ->> now():unix_timestamp() -= 1727654730[64] -``` diff --git a/docs/moments.md b/docs/moments.md new file mode 100644 index 00000000..1c3900c4 --- /dev/null +++ b/docs/moments.md @@ -0,0 +1,733 @@ +# Moments + +Tomo has a builtin datatype for representing a specific single point in time: +`Moment`. This is similar to a "datetime" in other languages, but it does not +represent a locale-specific time with a timezone. A Moment 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`). Moment +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 (`Moment.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 `Moment` 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. + +## Syntax + +Moment literals can be specified using [ISO +8601](https://en.wikipedia.org/wiki/ISO_8601) syntax with an optional +square-bracket delimited time zone name afterwards. A space may be used instead +of a `T` in the ISO 8601 format for readability, and spaces may come before the +timezone. + +```tomo +2024-09-30 +2024-09-30T13:57 +2024-09-30 13:57 +2024-09-30 13:57:01 +2024-09-30 13:57:01 +04:00 +2024-09-30 13:57:01 [America/New_York] +``` + +## Time Zones + +Because humans are not able to easily understand UNIX timestamps, the default +textual representation of `Moment` objects uses the current locale's +preferred representation of the Moment in the current time zone: + +```tomo +>> Moment.now() += Sun Sep 29 18:20:12 2024 EDT +``` + +For various methods, it is assumed by default that users wish to perform +calculations and specify moments using the local time zone and daylight +savings time rules. For example, if a program is running in New York and it is +currently 11pm on February 28th, 2023 (the last day of the month) in local +time, it is assumed that "one month from now" refers to 11pm on March 28th, +2024 in local time, rather than referring to one month from the current UTC +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 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 `UTC`. + +## Moment Methods + +### `after` + +**Description:** +Returns a Moment that occurs after the specified time differences. Time +differences may be either positive or negative. + +**Note:** time offsets for days, months, weeks, and years do not refer to fixed +time intervals, but are relative to which date they are applied to. For +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, +`timezone` which is used to determine in which timezone the offsets should be +calculated. + +**Signature:** +```tomo +func after(moment: Moment, 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 -> Moment) +``` + +**Parameters:** + +- `moment`: The moment used as a starting point. +- `seconds` (optional): An amount of seconds to offset the moment (default: 0). +- `minutes` (optional): An amount of minutes to offset the moment (default: 0). +- `hours` (optional): An amount of hours to offset the moment (default: 0). +- `days` (optional): An amount of days to offset the moment (default: 0). +- `weeks` (optional): An amount of weeks to offset the moment (default: 0). +- `months` (optional): An amount of months to offset the moment (default: 0). +- `years` (optional): An amount of years to offset the moment (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 `Moment` offset by the given amount. + +**Example:** +```tomo +>> Moment(2024, 9, 29, hour=19):after(days=1, minutes=30) += Mon Sep 30 19:30:00 2024 EDT +``` + +--- + +### `date` + +**Description:** +Return a text representation of the moment using the `"%F"` format +specifier, which gives the date in `YYYY-MM-DD` form. + +**Signature:** +```tomo +func date(moment: Moment, timezone : Text? = !Text -> Text) +``` + +**Parameters:** + +- `moment`: The moment to get the date from. +- `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. + +**Example:** +```tomo +>> Moment(2024, 9, 29):date() += "2024-09-29" +``` + +--- + +### `day_of_month` + +**Description:** +Return the integer day of the month (1-31). + +**Signature:** +```tomo +func day_of_month(moment: Moment, timezone : Text? = !Text -> Int) +``` + +**Parameters:** + +- `moment`: The moment to get the day of the month from. +- `timezone` (optional): If specified, use the given timezone (otherwise, use the current local timezone). + +**Returns:** +The day of the month as an integer (1-31). + +**Example:** +```tomo +>> Moment(2024, 9, 29):day_of_month() += 29 +``` + +--- + +### `day_of_week` + +**Description:** +Return the integer day of the week (1-7), where 1 = Sunday, 2 = Monday, +3 = Tuesday, 4 = Wednesday, 5 = Thursday, 6 = Friday, 7 = Saturday. + +**Signature:** +```tomo +func day_of_week(moment: Moment, timezone : Text? = !Text -> Int) +``` + +**Parameters:** + +- `moment`: The moment to get the day of the week from. +- `timezone` (optional): If specified, use the given timezone (otherwise, use the current local timezone). + +**Returns:** +The day of the week as an integer (1-7). + +**Example:** +```tomo +>> Moment(2024, 9, 29):day_of_week() += 1 +``` + +--- + +### `day_of_year` + +**Description:** +Return the integer day of the year (1-366, including leap years). + +**Signature:** +```tomo +func day_of_year(moment: Moment, timezone : Text? = !Text -> Int) +``` + +**Parameters:** + +- `moment`: The moment to get the day of the year from. +- `timezone` (optional): If specified, use the given timezone (otherwise, use the current local timezone). + +**Returns:** +The day of the year as an integer (1-366). + +**Example:** +```tomo +>> Moment(2024, 9, 29):day_of_year() += 272 +``` + +--- + +### `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 +`timezone` is specified, use that timezone instead of the current local +timezone. + +**Signature:** +```tomo +func format(moment: Moment, format: Text = "%Y-%m-%dT%H:%M:%S%z", timezone : Text? = !Text -> Text) +``` + +**Parameters:** + +- `moment`: The moment to format. +- `format`: The `strftime` format to use (default: `"%Y-%m-%dT%H:%M:%S%z"`). +- `timezone` (optional): If specified, use the given timezone (otherwise, use the current local timezone). + +**Returns:** +Nothing. + +**Example:** +```tomo +>> Moment(2024, 9, 29):format("%A") += "Sunday" +``` + +--- + +### `from_unix_timestamp` + +**Description:** +Return a moment object that represents the same moment in time as +the given UNIX epoch timestamp (seconds since January 1, 1970 UTC). + +**Signature:** +```tomo +func from_unix_timestamp(timestamp: Int64 -> Moment) +``` + +**Parameters:** + +- `timestamp`: The UNIX timestamp. + +**Returns:** +A `Moment` object representing the same moment as the given UNIX timestamp. + +**Example:** +```tomo +# In the New York timezone: +>> Moment.from_unix_timestamp(0) += Wed Dec 31 19:00:00 1969 +``` + +### `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 `Moment.set_local_timezone(...)`. + +**Signature:** +```tomo +func get_local_timezone(->Text) +``` + +**Parameters:** + +None. + +**Returns:** +The name of the current local timezone. + +**Example:** +```tomo +>> Moment.get_local_timezone() += "America/New_York" +``` + +--- + +### `hour` + +**Description:** +Return the hour of the day as an integer (1-24). + +**Signature:** +```tomo +func hour(moment: Moment, timezone : Text? = !Text -> Int) +``` + +**Parameters:** + +- `moment`: The moment to get the hour from. +- `timezone` (optional): If specified, use the given timezone (otherwise, use the current local timezone). + +**Returns:** +The hour of the day as an integer (1-24). + +**Example:** +```tomo +>> Moment(2024, 9, 29, 11, 59):hour() += 11 +``` + +--- + +### `hours_till` + +**Description:** +Return the number of hours until a given moment. + +**Signature:** +```tomo +func hours_till(moment: Moment, then:Moment -> Num) +``` + +**Parameters:** + +- `moment`: The starting point moment. +- `then`: Another moment that we want to calculate the time offset from (in hours). + +**Returns:** +The number of hours (possibly fractional, possibly negative) until the given time. + +**Example:** +```tomo +the_future := now():after(hours=1, minutes=30) +>> now():hours_till(the_future) += 1.5 +``` + +--- + +### `minute` + +**Description:** +Return the minute of the day as an integer (0-59). + +**Signature:** +```tomo +func minute(moment: Moment, timezone : Text? = !Text -> Int) +``` + +**Parameters:** + +- `moment`: The moment to get the minute from. +- `timezone` (optional): If specified, use the given timezone (otherwise, use the current local timezone). + +**Returns:** +The minute of the hour as an integer (0-59). + +**Example:** +```tomo +>> Moment(2024, 9, 29, 11, 59):minute() += 59 +``` + +--- + +### `minutes_till` + +**Description:** +Return the number of minutes until a given moment. + +**Signature:** +```tomo +func minutes_till(moment: Moment, then:Moment -> Num) +``` + +**Parameters:** + +- `moment`: The starting point moment. +- `then`: Another moment that we want to calculate the time offset from (in minutes). + +**Returns:** +The number of minutes (possibly fractional, possibly negative) until the given time. + +**Example:** +```tomo +the_future := now():after(minutes=1, seconds=30) +>> now():minutes_till(the_future) += 1.5 +``` + +--- + +### `month` + +**Description:** +Return the month of the year as an integer (1-12). + +**Signature:** +```tomo +func month(moment: Moment, timezone : Text? = !Text -> Int) +``` + +**Parameters:** + +- `moment`: The moment to get the month from. +- `timezone` (optional): If specified, use the given timezone (otherwise, use the current local timezone). + +**Returns:** +The month of the year as an integer (1-12). + +**Example:** +```tomo +>> Moment(2024, 9, 29, 11, 59):month() += 9 +``` + +--- + +### `nanosecond` + +**Description:** +Return the nanosecond of the second as an integer (0-999,999,999). + +**Signature:** +```tomo +func nanosecond(moment: Moment, timezone : Text? = !Text -> Int) +``` + +**Parameters:** + +- `moment`: The moment to get the nanosecond from. +- `timezone` (optional): If specified, use the given timezone (otherwise, use the current local timezone). + +**Returns:** +The nanosecond of the second as an integer (0-999,999,999). + +**Example:** +```tomo +>> Moment(2024, 9, 29, 11, 59):month() += 9 +``` + +--- + +### `new` + +**Description:** +Return a new `Moment` object representing the given time parameters expressed +in local time. This function is the same as calling `Moment` directly as a +constructor. + +**Signature:** +```tomo +func new(year : Int, month : Int, day : Int, hour : Int = 0, minute : Int = 0, second : Num = 0.0 -> Moment) +``` + +**Parameters:** + +- `year`: The year. +- `month`: The month of the year (1-12). +- `day`: The day of the month (1-31). +- `hour`: The hour of the day (0-23) (default: 0). +- `minute`: The minute of the hour (0-59) (default: 0). +- `second`: The second of the minute (0-59) (default: 0.0). + +**Returns:** +A `Moment` representing the given information in local time. If the given +parameters exceed reasonable bounds, the time values will wrap around. For +example, `Moment.new(..., hour=3, minute=65)` is the same as +`Moment.new(..., hour=4, minute=5)`. If any arguments cannot fit in a 32-bit +integer, an error will be raised. + +**Example:** +```tomo +>> Moment.new(2024, 9, 29) += Mon Sep 30 00:00:00 2024 EDT + +# March 1642, 2020: +>> Moment(2020, 4, 1643) += Sat Sep 28 00:00:00 2024 EDT +``` + +--- + +### `now` + +**Description:** +Get a `Moment` object representing the current date and time. This function +is the same as the global function `now()`. + +**Signature:** +```tomo +func now(->Moment) +``` + +**Parameters:** + +None. + +**Returns:** +Returns a `Moment` object representing the current date and time. + +**Example:** +```tomo +>> Moment.now() += Sun Sep 29 20:22:48 2024 EDT +``` + +--- + +### `parse` + +**Description:** +Return a new `Moment` object parsed from the given string in the given format, +or a null value if the value could not be successfully parsed. + +**Signature:** +```tomo +func parse(text: Text, format: Text = "%Y-%m-%dT%H:%M:%S%z" -> Moment?) +``` + +**Parameters:** + +- `text`: The text to parse. +- `format`: The date format of the text being parsed (see: + [strptime](https://linux.die.net/man/3/strptime) for more info on this + format) (default: `"%Y-%m-%dT%H:%M:%S%z"`). + +**Returns:** +If the text was successfully parsed according to the given format, return a +`Moment` representing that information. Otherwise, return a null value. + +**Example:** +```tomo +>> Moment.parse("2024-09-29", "%Y-%m-%d")! += Sun Sep 29 00:00:00 2024 EDT + +>> Moment.parse("???", "%Y-%m-%d") += !Moment +``` + +--- + +### `relative` + +**Description:** +Return a plain English textual representation of the approximate time difference +between two `Moment`s. For example: `5 minutes ago` or `1 day later` + +**Signature:** +```tomo +func relative(moment: Moment, relative_to : Moment = Moment.now(), timezone : Text? = !Text -> Text) +``` + +**Parameters:** + +- `moment`: The moment whose relative time you're getting. +- `relative_to` (optional): The time against which the relative time is calculated (default: `Moment.now()`). +- `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 +difference between two `Moment`s. For example: `5 minutes ago` or `1 day +later`. Return values are approximate and use only one significant unit of +measure with one significant digit, so a difference of 1.6 days will be +represented as `2 days later`. moments in the past will have the suffix `" +ago"`, while moments in the future will have the suffix `" later"`. + +**Example:** +```tomo +>> now():after(days=2):relative() += "2 days later" + +>> now():after(minutes=-65):relative() += "1 hour ago" +``` + +--- + +### `second` + +**Description:** +Return the second of the minute as an integer (0-59). + +**Signature:** +```tomo +func second(moment: Moment, timezone : Text? = !Text -> Int) +``` + +**Parameters:** + +- `moment`: The moment to get the second from. +- `timezone` (optional): If specified, use the given timezone (otherwise, use the current local timezone). + +**Returns:** +The second of the hour as an integer (0-59). + +**Example:** +```tomo +>> Moment(2024, 9, 29, 11, 30, 59):second() += 59 +``` + +--- + +### `seconds_till` + +**Description:** +Return the number of seconds until a given moment. + +**Signature:** +```tomo +func seconds_till(moment: Moment, then:Moment -> Num) +``` + +**Parameters:** + +- `moment`: The starting point moment. +- `then`: Another moment that we want to calculate the time offset from (in seconds). + +**Returns:** +The number of seconds (possibly fractional, possibly negative) until the given time. + +**Example:** +```tomo +the_future := now():after(seconds=1) +>> now():seconds_till(the_future) += 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 `Moment` objects from +component parts. It's also used as the default way that `Moment` objects are +converted to text. + +**Signature:** +```tomo +func 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:** +```tomo +Moment.set_local_timezone("America/Los_Angeles") +``` + +--- + +### `time` + +**Description:** +Return a text representation of the time component of the given moment. + +**Signature:** +```tomo +func time(moment: Moment, seconds : Bool = no, am_pm : Bool = yes, timezone : Text? = !Text -> Text) +``` + +**Parameters:** + +- `moment`: The moment whose time value you want to get. +- `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`). +- `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 moment. + +**Example:** +```tomo +moment := Moment(2024, 9, 29, hours=13, minutes=59, seconds=30) + +>> dt:time() += "1:59pm" + +>> dt:time(am_pm=no) += "13:59" + +>> dt:time(seconds=yes) += "1:59:30pm" +``` + +--- + +### `unix_timestamp` + +**Description:** +Get the UNIX timestamp of the given moment (seconds since the UNIX epoch: +January 1, 1970 UTC). + +**Signature:** +```tomo +func unix_timestamp(moment:Moment->Int64) +``` + +**Parameters:** + +`moment`: The moment whose UNIX timestamp you want to get. + +**Returns:** +A 64-bit integer representation of the UNIX timestamp. + +**Example:** +```tomo +>> now():unix_timestamp() += 1727654730[64] +``` diff --git a/environment.c b/environment.c index b6a9e7d4..3781b4d4 100644 --- a/environment.c +++ b/environment.c @@ -53,7 +53,7 @@ env_t *new_compilation_unit(CORD libname) .ret=Type(AbortType))}}, {"fail", {.code="fail", .type=Type(FunctionType, .args=new(arg_t, .name="message", .type=Type(CStringType)), .ret=Type(AbortType))}}, {"sleep", {.code="sleep_num", .type=Type(FunctionType, .args=new(arg_t, .name="seconds", .type=Type(NumType, .bits=TYPE_NBITS64)), .ret=Type(VoidType))}}, - {"now", {.code="DateTime$now", .type=Type(FunctionType, .args=NULL, .ret=Type(DateTimeType))}}, + {"now", {.code="Moment$now", .type=Type(FunctionType, .args=NULL, .ret=Type(MomentType))}}, {"USE_COLOR", {.code="USE_COLOR", .type=Type(BoolType)}}, }; @@ -287,33 +287,33 @@ env_t *new_compilation_unit(CORD libname) {"escape_int", "Int$value_as_text", "func(i:Int -> Pattern)"}, {"escape_text", "Pattern$escape_text", "func(text:Text -> Pattern)"}, )}, - {"DateTime", Type(DateTimeType), "DateTime_t", "DateTime", TypedArray(ns_entry_t, + {"Moment", Type(MomentType), "Moment_t", "Moment", TypedArray(ns_entry_t, // Used as a default for functions below: - {"now", "DateTime$now", "func(->DateTime)"}, - - {"after", "DateTime$after", "func(dt:DateTime,seconds,minutes,hours=0.0,days,weeks,months,years=0,timezone=!Text -> DateTime)"}, - {"date", "DateTime$date", "func(dt:DateTime,timezone=!Text -> Text)"}, - {"day_of_month", "DateTime$day_of_month", "func(dt:DateTime,timezone=!Text -> Int)"}, - {"day_of_week", "DateTime$day_of_week", "func(dt:DateTime,timezone=!Text -> Int)"}, - {"day_of_year", "DateTime$day_of_year", "func(dt:DateTime,timezone=!Text -> Int)"}, - {"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_local_timezone", "DateTime$get_local_timezone", "func(->Text)"}, - {"hour", "DateTime$hour", "func(dt:DateTime,timezone=!Text -> Int)"}, - {"hours_till", "DateTime$hours_till", "func(now,then:DateTime -> Num)"}, - {"minute", "DateTime$minute", "func(dt:DateTime,timezone=!Text -> Int)"}, - {"minutes_till", "DateTime$minutes_till", "func(now,then:DateTime -> Num)"}, - {"month", "DateTime$month", "func(dt:DateTime,timezone=!Text -> Int)"}, - {"nanosecond", "DateTime$nanosecond", "func(dt:DateTime,timezone=!Text -> Int)"}, - {"new", "DateTime$new", "func(year,month,day:Int,hour,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(),timezone=!Text -> Text)"}, - {"second", "DateTime$second", "func(dt:DateTime,timezone=!Text -> Int)"}, - {"seconds_till", "DateTime$seconds_till", "func(now:DateTime,then:DateTime -> Num)"}, - {"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)"}, - {"year", "DateTime$year", "func(dt:DateTime,timezone=!Text -> Int)"}, + {"now", "Moment$now", "func(->Moment)"}, + + {"after", "Moment$after", "func(moment:Moment,seconds,minutes,hours=0.0,days,weeks,months,years=0,timezone=!Text -> Moment)"}, + {"date", "Moment$date", "func(moment:Moment,timezone=!Text -> Text)"}, + {"day_of_month", "Moment$day_of_month", "func(moment:Moment,timezone=!Text -> Int)"}, + {"day_of_week", "Moment$day_of_week", "func(moment:Moment,timezone=!Text -> Int)"}, + {"day_of_year", "Moment$day_of_year", "func(moment:Moment,timezone=!Text -> Int)"}, + {"format", "Moment$format", "func(moment:Moment,format=\"%Y-%m-%dT%H:%M:%S%z\",timezone=!Text -> Text)"}, + {"from_unix_timestamp", "Moment$from_unix_timestamp", "func(timestamp:Int64 -> Moment)"}, + {"get_local_timezone", "Moment$get_local_timezone", "func(->Text)"}, + {"hour", "Moment$hour", "func(moment:Moment,timezone=!Text -> Int)"}, + {"hours_till", "Moment$hours_till", "func(now,then:Moment -> Num)"}, + {"minute", "Moment$minute", "func(moment:Moment,timezone=!Text -> Int)"}, + {"minutes_till", "Moment$minutes_till", "func(now,then:Moment -> Num)"}, + {"month", "Moment$month", "func(moment:Moment,timezone=!Text -> Int)"}, + {"nanosecond", "Moment$nanosecond", "func(moment:Moment,timezone=!Text -> Int)"}, + {"new", "Moment$new", "func(year,month,day:Int,hour,minute=0,second=0.0,timezone=!Text -> Moment)"}, + {"parse", "Moment$parse", "func(text:Text, format=\"%Y-%m-%dT%H:%M:%S%z\" -> Moment?)"}, + {"relative", "Moment$relative", "func(moment:Moment,relative_to=Moment.now(),timezone=!Text -> Text)"}, + {"second", "Moment$second", "func(moment:Moment,timezone=!Text -> Int)"}, + {"seconds_till", "Moment$seconds_till", "func(now:Moment,then:Moment -> Num)"}, + {"set_local_timezone", "Moment$set_local_timezone", "func(timezone=!Text)"}, + {"time", "Moment$time", "func(moment:Moment,seconds=no,am_pm=yes,timezone=!Text -> Text)"}, + {"unix_timestamp", "Moment$unix_timestamp", "func(moment:Moment -> Int64)"}, + {"year", "Moment$year", "func(moment:Moment,timezone=!Text -> Int)"}, )}, {"Path", Type(TextType, .lang="Path", .env=namespace_env(env, "Path")), "Text_t", "Text$info", TypedArray(ns_entry_t, {"append", "Path$append", "func(path:Path, text:Text, permissions=Int32(0o644))"}, @@ -347,9 +347,9 @@ env_t *new_compilation_unit(CORD libname) {"write_unique", "Path$write_unique", "func(path:Path, text:Text -> Path)"}, {"write_unique_bytes", "Path$write_unique_bytes", "func(path:Path, bytes:[Byte] -> Path)"}, - {"modified", "Path$modified", "func(path:Path, follow_symlinks=yes -> DateTime?)"}, - {"accessed", "Path$accessed", "func(path:Path, follow_symlinks=yes -> DateTime?)"}, - {"changed", "Path$changed", "func(path:Path, follow_symlinks=yes -> DateTime?)"}, + {"modified", "Path$modified", "func(path:Path, follow_symlinks=yes -> Moment?)"}, + {"accessed", "Path$accessed", "func(path:Path, follow_symlinks=yes -> Moment?)"}, + {"changed", "Path$changed", "func(path:Path, follow_symlinks=yes -> Moment?)"}, // Text methods: {"ends_with", "Text$ends_with", "func(path:Path, suffix:Text -> Bool)"}, @@ -657,7 +657,7 @@ binding_t *get_namespace_binding(env_t *env, ast_t *self, const char *name) switch (cls_type->tag) { case ArrayType: return NULL; case TableType: return NULL; - case CStringType: case DateTimeType: + case CStringType: case MomentType: case BoolType: case IntType: case BigIntType: case NumType: case ByteType: { binding_t *b = get_binding(env, CORD_to_const_char_star(type_to_cord(cls_type))); assert(b); diff --git a/parse.c b/parse.c index 1272fe64..5932fba0 100644 --- a/parse.c +++ b/parse.c @@ -110,7 +110,7 @@ static PARSER(parse_heap_alloc); static PARSER(parse_if); static PARSER(parse_inline_c); static PARSER(parse_int); -static PARSER(parse_datetime); +static PARSER(parse_moment); static PARSER(parse_lambda); static PARSER(parse_lang_def); static PARSER(parse_namespace); @@ -508,7 +508,7 @@ PARSER(parse_int) { return NewAST(ctx->file, start, pos, Int, .str=str); } -PARSER(parse_datetime) { +PARSER(parse_moment) { const char *start = pos; bool negative = match(&pos, "-"); if (!isdigit(*pos)) return NULL; @@ -530,17 +530,17 @@ PARSER(parse_datetime) { const char *before_spaces = pos; spaces(&pos); - DateTime_t dt; + Moment_t moment; if (match(&pos, "[")) { size_t tz_len = strcspn(pos, "\r\n]"); const char *tz = heap_strf("%.*s", tz_len, pos); // TODO: check that tz is a valid timezone pos += tz_len; - expect_closing(ctx, &pos, "]", "I wasn't able to parse the rest of this datetime timezone"); + expect_closing(ctx, &pos, "]", "I wasn't able to parse the rest of this moment timezone"); const char *old_tz = getenv("TZ"); setenv("TZ", tz, 1); tzset(); - dt = (DateTime_t){.tv_sec=mktime(&info)}; + moment = (Moment_t){.tv_sec=mktime(&info)}; if (old_tz) setenv("TZ", old_tz, 1); else unsetenv("TZ"); } else if (*pos == 'Z' || *pos == '-' || *pos == '+') { @@ -549,16 +549,16 @@ PARSER(parse_datetime) { pos = after; long offset = info.tm_gmtoff; // Need to cache this because mktime() mutates it to local timezone >:( time_t t = mktime(&info); - dt = (DateTime_t){.tv_sec=t + offset - info.tm_gmtoff}; + moment = (Moment_t){.tv_sec=t + offset - info.tm_gmtoff}; } else { - dt = (DateTime_t){.tv_sec=mktime(&info)}; + moment = (Moment_t){.tv_sec=mktime(&info)}; } } else { pos = before_spaces; - dt = (DateTime_t){.tv_sec=mktime(&info)}; + moment = (Moment_t){.tv_sec=mktime(&info)}; } - return NewAST(ctx->file, start, pos, DateTime, .dt=dt); + return NewAST(ctx->file, start, pos, Moment, .moment=moment); } type_ast_t *parse_table_type(parse_ctx_t *ctx, const char *pos) { @@ -1553,7 +1553,7 @@ PARSER(parse_term_no_suffix) { ast_t *term = NULL; (void)( false - || (term=parse_datetime(ctx, pos)) // Must come before num/int + || (term=parse_moment(ctx, pos)) // Must come before num/int || (term=parse_null(ctx, pos)) || (term=parse_num(ctx, pos)) // Must come before int || (term=parse_int(ctx, pos)) diff --git a/stdlib/datatypes.h b/stdlib/datatypes.h index f033a21a..5dddaa0f 100644 --- a/stdlib/datatypes.h +++ b/stdlib/datatypes.h @@ -90,8 +90,8 @@ typedef struct Text_s { #define Pattern_t Text_t #define OptionalPattern_t Text_t -typedef struct timeval DateTime_t; -#define OptionalDateTime_t DateTime_t +typedef struct timeval Moment_t; +#define OptionalMoment_t Moment_t typedef struct RNGState_t* RNG_t; diff --git a/stdlib/datetime.c b/stdlib/datetime.c deleted file mode 100644 index b73c583e..00000000 --- a/stdlib/datetime.c +++ /dev/null @@ -1,317 +0,0 @@ -// DateTime methods/type info -#include -#include -#include -#include -#include -#include - -#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; - -#define WITH_TIMEZONE(tz, body) ({ if (tz.length >= 0) { \ - OptionalText_t old_timezone = _local_timezone; \ - DateTime$set_local_timezone(tz); \ - body; \ - DateTime$set_local_timezone(old_timezone); \ - } else { \ - body; \ - }}) - -public Text_t DateTime$as_text(const DateTime_t *dt, bool colorize, const TypeInfo_t *type) -{ - (void)type; - if (!dt) - return Text("DateTime"); - - struct tm info; - struct tm *final_info = localtime_r(&dt->tv_sec, &info); - static char buf[256]; - size_t len = strftime(buf, sizeof(buf), "%c %Z", final_info); - Text_t text = Text$format("%.*s", (int)len, buf); - if (colorize) - text = Text$concat(Text("\x1b[36m"), text, Text("\x1b[m")); - return text; -} - -PUREFUNC public int32_t DateTime$compare(const DateTime_t *a, const DateTime_t *b, const TypeInfo_t *type) -{ - (void)type; - if (a->tv_sec != b->tv_sec) - return (a->tv_sec > b->tv_sec) - (a->tv_sec < b->tv_sec); - return (a->tv_usec > b->tv_usec) - (a->tv_usec < b->tv_usec); -} - -public DateTime_t DateTime$now(void) -{ - struct timespec ts; - if (clock_gettime(CLOCK_REALTIME, &ts) != 0) - fail("Couldn't get the time!"); - 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, OptionalText_t timezone) -{ - struct tm info = { - .tm_min=Int_to_Int32(minute, false), - .tm_hour=Int_to_Int32(hour, false), - .tm_mday=Int_to_Int32(day, false), - .tm_mon=Int_to_Int32(month, false) - 1, - .tm_year=Int_to_Int32(year, false) - 1900, - .tm_isdst=-1, - }; - - time_t t; - WITH_TIMEZONE(timezone, t = mktime(&info)); - 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, OptionalText_t timezone) -{ - double offset = seconds + 60.*minutes + 3600.*hours; - dt.tv_sec += (time_t)offset; - - struct tm info = {}; - WITH_TIMEZONE(timezone, 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); - - time_t t = mktime(&info); - return (DateTime_t){ - .tv_sec=t, - .tv_usec=dt.tv_usec + (suseconds_t)(fmod(offset, 1.0) * 1e9), - }; -} - -CONSTFUNC public double DateTime$seconds_till(DateTime_t now, DateTime_t then) -{ - return (double)(then.tv_sec - now.tv_sec) + 1e-9*(double)(then.tv_usec - now.tv_usec); -} - -CONSTFUNC public double DateTime$minutes_till(DateTime_t now, DateTime_t then) -{ - return DateTime$seconds_till(now, then)/60.; -} - -CONSTFUNC public double DateTime$hours_till(DateTime_t now, DateTime_t then) -{ - return DateTime$seconds_till(now, then)/3600.; -} - -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, OptionalText_t timezone) -{ - struct tm info = {}; - WITH_TIMEZONE(timezone, localtime_r(&dt.tv_sec, &info)); - - if (year) *year = I(info.tm_year + 1900); - if (month) *month = I(info.tm_mon + 1); - if (day) *day = I(info.tm_mday); - if (hour) *hour = I(info.tm_hour); - if (minute) *minute = I(info.tm_min); - if (second) *second = I(info.tm_sec); - if (nanosecond) *nanosecond = I(dt.tv_usec); - if (weekday) *weekday = I(info.tm_wday + 1); -} - -public Int_t DateTime$year(DateTime_t dt, OptionalText_t timezone) -{ - struct tm info = {}; - WITH_TIMEZONE(timezone, localtime_r(&dt.tv_sec, &info)); - return I(info.tm_year + 1900); -} - -public Int_t DateTime$month(DateTime_t dt, OptionalText_t timezone) -{ - struct tm info = {}; - WITH_TIMEZONE(timezone, localtime_r(&dt.tv_sec, &info)); - return I(info.tm_mon + 1); -} - -public Int_t DateTime$day_of_week(DateTime_t dt, OptionalText_t timezone) -{ - struct tm info = {}; - WITH_TIMEZONE(timezone, localtime_r(&dt.tv_sec, &info)); - return I(info.tm_wday + 1); -} - -public Int_t DateTime$day_of_month(DateTime_t dt, OptionalText_t timezone) -{ - struct tm info = {}; - WITH_TIMEZONE(timezone, localtime_r(&dt.tv_sec, &info)); - return I(info.tm_mday); -} - -public Int_t DateTime$day_of_year(DateTime_t dt, OptionalText_t timezone) -{ - struct tm info = {}; - WITH_TIMEZONE(timezone, localtime_r(&dt.tv_sec, &info)); - return I(info.tm_yday); -} - -public Int_t DateTime$hour(DateTime_t dt, OptionalText_t timezone) -{ - struct tm info = {}; - WITH_TIMEZONE(timezone, localtime_r(&dt.tv_sec, &info)); - return I(info.tm_hour); -} - -public Int_t DateTime$minute(DateTime_t dt, OptionalText_t timezone) -{ - struct tm info = {}; - WITH_TIMEZONE(timezone, localtime_r(&dt.tv_sec, &info)); - return I(info.tm_min); -} - -public Int_t DateTime$second(DateTime_t dt, OptionalText_t timezone) -{ - struct tm info = {}; - WITH_TIMEZONE(timezone, localtime_r(&dt.tv_sec, &info)); - return I(info.tm_sec); -} - -public Int_t DateTime$nanosecond(DateTime_t dt, OptionalText_t timezone) -{ - (void)timezone; - return I(dt.tv_usec); -} - -public Text_t DateTime$format(DateTime_t dt, Text_t fmt, OptionalText_t timezone) -{ - struct tm info; - WITH_TIMEZONE(timezone, localtime_r(&dt.tv_sec, &info)); - static char buf[256]; - 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, OptionalText_t timezone) -{ - return DateTime$format(dt, Text("%F"), timezone); -} - -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"), timezone); - else - text = DateTime$format(dt, am_pm ? Text("%l:%M%P") : Text("%H:%M"), timezone); - return Text$trim(text, Pattern(" "), true, true); -} - -public OptionalDateTime_t DateTime$parse(Text_t text, Text_t format) -{ - struct tm info = {.tm_isdst=-1}; - const char *str = Text$as_c_string(text); - const char *fmt = Text$as_c_string(format); - if (strstr(fmt, "%Z")) - fail("The %%Z specifier is not supported for time parsing!"); - - char *invalid = strptime(str, fmt, &info); - if (!invalid || invalid[0] != '\0') - return NULL_DATETIME; - - long offset = info.tm_gmtoff; // Need to cache this because mktime() mutates it to local timezone >:( - time_t t = mktime(&info); - return (DateTime_t){.tv_sec=t + offset - info.tm_gmtoff}; -} - -static INLINE Text_t num_format(long n, const char *unit) -{ - if (n == 0) - return Text("now"); - 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, OptionalText_t timezone) -{ - struct tm info = {}; - struct tm relative_info = {}; - WITH_TIMEZONE(timezone, { - 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.) - return num_format((long)info.tm_year - (long)relative_info.tm_year, "year"); - else if (info.tm_mon != relative_info.tm_mon && fabs(second_diff) > 31.*24.*60.*60.) - return num_format(12*((long)info.tm_year - (long)relative_info.tm_year) + (long)info.tm_mon - (long)relative_info.tm_mon, "month"); - else if (info.tm_yday != relative_info.tm_yday && fabs(second_diff) > 24.*60.*60.) - return num_format(round(second_diff/(24.*60.*60.)), "day"); - else if (info.tm_hour != relative_info.tm_hour && fabs(second_diff) > 60.*60.) - return num_format(round(second_diff/(60.*60.)), "hour"); - else if (info.tm_min != relative_info.tm_min && fabs(second_diff) > 60.) - return num_format(round(second_diff/(60.)), "minute"); - else { - if (fabs(second_diff) < 1e-6) - return num_format((long)(second_diff*1e9), "nanosecond"); - else if (fabs(second_diff) < 1e-3) - return num_format((long)(second_diff*1e6), "microsecond"); - else if (fabs(second_diff) < 1.0) - return num_format((long)(second_diff*1e3), "millisecond"); - else - return num_format((long)(second_diff), "second"); - } -} - -CONSTFUNC public Int64_t DateTime$unix_timestamp(DateTime_t dt) -{ - return (Int64_t)dt.tv_sec; -} - -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_t DateTime$info = { - .size=sizeof(DateTime_t), - .align=__alignof__(DateTime_t), - .tag=CustomInfo, - .CustomInfo={ - .as_text=(void*)DateTime$as_text, - .compare=(void*)DateTime$compare, - }, -}; - -// vim: ts=4 sw=0 et cino=L2,l1,(0,W4,m1,\:0 diff --git a/stdlib/datetime.h b/stdlib/datetime.h deleted file mode 100644 index ccbc5190..00000000 --- a/stdlib/datetime.h +++ /dev/null @@ -1,43 +0,0 @@ -#pragma once - -// DateTime objects - -#include - -#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_t *type); -PUREFUNC int32_t DateTime$compare(const DateTime_t *a, const DateTime_t *b, const TypeInfo_t *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, 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); -Int_t DateTime$year(DateTime_t dt, OptionalText_t timezone); -Int_t DateTime$month(DateTime_t dt, OptionalText_t timezone); -Int_t DateTime$day_of_week(DateTime_t dt, OptionalText_t timezone); -Int_t DateTime$day_of_month(DateTime_t dt, OptionalText_t timezone); -Int_t DateTime$day_of_year(DateTime_t dt, OptionalText_t timezone); -Int_t DateTime$hour(DateTime_t dt, OptionalText_t timezone); -Int_t DateTime$minute(DateTime_t dt, OptionalText_t timezone); -Int_t DateTime$second(DateTime_t dt, OptionalText_t timezone); -Int_t DateTime$nanosecond(DateTime_t dt, 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, 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_t DateTime$info; - -// vim: ts=4 sw=0 et cino=L2,l1,(0,W4,m1,\:0 - diff --git a/stdlib/moments.c b/stdlib/moments.c new file mode 100644 index 00000000..955d5e93 --- /dev/null +++ b/stdlib/moments.c @@ -0,0 +1,317 @@ +// Moment methods/type info +#include +#include +#include +#include +#include +#include + +#include "datatypes.h" +#include "moments.h" +#include "optionals.h" +#include "patterns.h" +#include "stdlib.h" +#include "text.h" +#include "util.h" + +static OptionalText_t _local_timezone = NULL_TEXT; + +#define WITH_TIMEZONE(tz, body) ({ if (tz.length >= 0) { \ + OptionalText_t old_timezone = _local_timezone; \ + Moment$set_local_timezone(tz); \ + body; \ + Moment$set_local_timezone(old_timezone); \ + } else { \ + body; \ + }}) + +public Text_t Moment$as_text(const Moment_t *moment, bool colorize, const TypeInfo_t *type) +{ + (void)type; + if (!moment) + return Text("Moment"); + + struct tm info; + struct tm *final_info = localtime_r(&moment->tv_sec, &info); + static char buf[256]; + size_t len = strftime(buf, sizeof(buf), "%c %Z", final_info); + Text_t text = Text$format("%.*s", (int)len, buf); + if (colorize) + text = Text$concat(Text("\x1b[36m"), text, Text("\x1b[m")); + return text; +} + +PUREFUNC public int32_t Moment$compare(const Moment_t *a, const Moment_t *b, const TypeInfo_t *type) +{ + (void)type; + if (a->tv_sec != b->tv_sec) + return (a->tv_sec > b->tv_sec) - (a->tv_sec < b->tv_sec); + return (a->tv_usec > b->tv_usec) - (a->tv_usec < b->tv_usec); +} + +public Moment_t Moment$now(void) +{ + struct timespec ts; + if (clock_gettime(CLOCK_REALTIME, &ts) != 0) + fail("Couldn't get the time!"); + return (Moment_t){.tv_sec=ts.tv_sec, .tv_usec=ts.tv_nsec}; +} + +public Moment_t Moment$new(Int_t year, Int_t month, Int_t day, Int_t hour, Int_t minute, double second, OptionalText_t timezone) +{ + struct tm info = { + .tm_min=Int_to_Int32(minute, false), + .tm_hour=Int_to_Int32(hour, false), + .tm_mday=Int_to_Int32(day, false), + .tm_mon=Int_to_Int32(month, false) - 1, + .tm_year=Int_to_Int32(year, false) - 1900, + .tm_isdst=-1, + }; + + time_t t; + WITH_TIMEZONE(timezone, t = mktime(&info)); + return (Moment_t){.tv_sec=t + (time_t)second, .tv_usec=(suseconds_t)(fmod(second, 1.0) * 1e9)}; +} + +public Moment_t Moment$after(Moment_t moment, 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; + moment.tv_sec += (time_t)offset; + + struct tm info = {}; + WITH_TIMEZONE(timezone, localtime_r(&moment.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); + + time_t t = mktime(&info); + return (Moment_t){ + .tv_sec=t, + .tv_usec=moment.tv_usec + (suseconds_t)(fmod(offset, 1.0) * 1e9), + }; +} + +CONSTFUNC public double Moment$seconds_till(Moment_t now, Moment_t then) +{ + return (double)(then.tv_sec - now.tv_sec) + 1e-9*(double)(then.tv_usec - now.tv_usec); +} + +CONSTFUNC public double Moment$minutes_till(Moment_t now, Moment_t then) +{ + return Moment$seconds_till(now, then)/60.; +} + +CONSTFUNC public double Moment$hours_till(Moment_t now, Moment_t then) +{ + return Moment$seconds_till(now, then)/3600.; +} + +public void Moment$get( + Moment_t moment, 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) +{ + struct tm info = {}; + WITH_TIMEZONE(timezone, localtime_r(&moment.tv_sec, &info)); + + if (year) *year = I(info.tm_year + 1900); + if (month) *month = I(info.tm_mon + 1); + if (day) *day = I(info.tm_mday); + if (hour) *hour = I(info.tm_hour); + if (minute) *minute = I(info.tm_min); + if (second) *second = I(info.tm_sec); + if (nanosecond) *nanosecond = I(moment.tv_usec); + if (weekday) *weekday = I(info.tm_wday + 1); +} + +public Int_t Moment$year(Moment_t moment, OptionalText_t timezone) +{ + struct tm info = {}; + WITH_TIMEZONE(timezone, localtime_r(&moment.tv_sec, &info)); + return I(info.tm_year + 1900); +} + +public Int_t Moment$month(Moment_t moment, OptionalText_t timezone) +{ + struct tm info = {}; + WITH_TIMEZONE(timezone, localtime_r(&moment.tv_sec, &info)); + return I(info.tm_mon + 1); +} + +public Int_t Moment$day_of_week(Moment_t moment, OptionalText_t timezone) +{ + struct tm info = {}; + WITH_TIMEZONE(timezone, localtime_r(&moment.tv_sec, &info)); + return I(info.tm_wday + 1); +} + +public Int_t Moment$day_of_month(Moment_t moment, OptionalText_t timezone) +{ + struct tm info = {}; + WITH_TIMEZONE(timezone, localtime_r(&moment.tv_sec, &info)); + return I(info.tm_mday); +} + +public Int_t Moment$day_of_year(Moment_t moment, OptionalText_t timezone) +{ + struct tm info = {}; + WITH_TIMEZONE(timezone, localtime_r(&moment.tv_sec, &info)); + return I(info.tm_yday); +} + +public Int_t Moment$hour(Moment_t moment, OptionalText_t timezone) +{ + struct tm info = {}; + WITH_TIMEZONE(timezone, localtime_r(&moment.tv_sec, &info)); + return I(info.tm_hour); +} + +public Int_t Moment$minute(Moment_t moment, OptionalText_t timezone) +{ + struct tm info = {}; + WITH_TIMEZONE(timezone, localtime_r(&moment.tv_sec, &info)); + return I(info.tm_min); +} + +public Int_t Moment$second(Moment_t moment, OptionalText_t timezone) +{ + struct tm info = {}; + WITH_TIMEZONE(timezone, localtime_r(&moment.tv_sec, &info)); + return I(info.tm_sec); +} + +public Int_t Moment$nanosecond(Moment_t moment, OptionalText_t timezone) +{ + (void)timezone; + return I(moment.tv_usec); +} + +public Text_t Moment$format(Moment_t moment, Text_t fmt, OptionalText_t timezone) +{ + struct tm info; + WITH_TIMEZONE(timezone, localtime_r(&moment.tv_sec, &info)); + static char buf[256]; + size_t len = strftime(buf, sizeof(buf), Text$as_c_string(fmt), &info); + return Text$format("%.*s", (int)len, buf); +} + +public Text_t Moment$date(Moment_t moment, OptionalText_t timezone) +{ + return Moment$format(moment, Text("%F"), timezone); +} + +public Text_t Moment$time(Moment_t moment, bool seconds, bool am_pm, OptionalText_t timezone) +{ + Text_t text; + if (seconds) + text = Moment$format(moment, am_pm ? Text("%l:%M:%S%P") : Text("%T"), timezone); + else + text = Moment$format(moment, am_pm ? Text("%l:%M%P") : Text("%H:%M"), timezone); + return Text$trim(text, Pattern(" "), true, true); +} + +public OptionalMoment_t Moment$parse(Text_t text, Text_t format) +{ + struct tm info = {.tm_isdst=-1}; + const char *str = Text$as_c_string(text); + const char *fmt = Text$as_c_string(format); + if (strstr(fmt, "%Z")) + fail("The %%Z specifier is not supported for time parsing!"); + + char *invalid = strptime(str, fmt, &info); + if (!invalid || invalid[0] != '\0') + return NULL_MOMENT; + + long offset = info.tm_gmtoff; // Need to cache this because mktime() mutates it to local timezone >:( + time_t t = mktime(&info); + return (Moment_t){.tv_sec=t + offset - info.tm_gmtoff}; +} + +static INLINE Text_t num_format(long n, const char *unit) +{ + if (n == 0) + return Text("now"); + 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 Moment$relative(Moment_t moment, Moment_t relative_to, OptionalText_t timezone) +{ + struct tm info = {}; + struct tm relative_info = {}; + WITH_TIMEZONE(timezone, { + localtime_r(&moment.tv_sec, &info); + localtime_r(&relative_to.tv_sec, &relative_info); + }); + + double second_diff = Moment$seconds_till(relative_to, moment); + if (info.tm_year != relative_info.tm_year && fabs(second_diff) > 365.*24.*60.*60.) + return num_format((long)info.tm_year - (long)relative_info.tm_year, "year"); + else if (info.tm_mon != relative_info.tm_mon && fabs(second_diff) > 31.*24.*60.*60.) + return num_format(12*((long)info.tm_year - (long)relative_info.tm_year) + (long)info.tm_mon - (long)relative_info.tm_mon, "month"); + else if (info.tm_yday != relative_info.tm_yday && fabs(second_diff) > 24.*60.*60.) + return num_format(round(second_diff/(24.*60.*60.)), "day"); + else if (info.tm_hour != relative_info.tm_hour && fabs(second_diff) > 60.*60.) + return num_format(round(second_diff/(60.*60.)), "hour"); + else if (info.tm_min != relative_info.tm_min && fabs(second_diff) > 60.) + return num_format(round(second_diff/(60.)), "minute"); + else { + if (fabs(second_diff) < 1e-6) + return num_format((long)(second_diff*1e9), "nanosecond"); + else if (fabs(second_diff) < 1e-3) + return num_format((long)(second_diff*1e6), "microsecond"); + else if (fabs(second_diff) < 1.0) + return num_format((long)(second_diff*1e3), "millisecond"); + else + return num_format((long)(second_diff), "second"); + } +} + +CONSTFUNC public Int64_t Moment$unix_timestamp(Moment_t moment) +{ + return (Int64_t)moment.tv_sec; +} + +CONSTFUNC public Moment_t Moment$from_unix_timestamp(Int64_t timestamp) +{ + return (Moment_t){.tv_sec=(time_t)timestamp}; +} + +public void Moment$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 Moment$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_t Moment$info = { + .size=sizeof(Moment_t), + .align=__alignof__(Moment_t), + .tag=CustomInfo, + .CustomInfo={ + .as_text=(void*)Moment$as_text, + .compare=(void*)Moment$compare, + }, +}; + +// vim: ts=4 sw=0 et cino=L2,l1,(0,W4,m1,\:0 diff --git a/stdlib/moments.h b/stdlib/moments.h new file mode 100644 index 00000000..8e423124 --- /dev/null +++ b/stdlib/moments.h @@ -0,0 +1,43 @@ +#pragma once + +// Moment objects + +#include + +#include "datatypes.h" +#include "integers.h" +#include "optionals.h" +#include "types.h" +#include "util.h" + +Text_t Moment$as_text(const Moment_t *moment, bool colorize, const TypeInfo_t *type); +PUREFUNC int32_t Moment$compare(const Moment_t *a, const Moment_t *b, const TypeInfo_t *type); +Moment_t Moment$now(void); +Moment_t Moment$new(Int_t year, Int_t month, Int_t day, Int_t hour, Int_t minute, double second, OptionalText_t timezone); +Moment_t Moment$after(Moment_t moment, double seconds, double minutes, double hours, Int_t days, Int_t weeks, Int_t months, Int_t years, OptionalText_t timezone); +CONSTFUNC double Moment$seconds_till(Moment_t now, Moment_t then); +CONSTFUNC double Moment$minutes_till(Moment_t now, Moment_t then); +CONSTFUNC double Moment$hours_till(Moment_t now, Moment_t then); +Int_t Moment$year(Moment_t moment, OptionalText_t timezone); +Int_t Moment$month(Moment_t moment, OptionalText_t timezone); +Int_t Moment$day_of_week(Moment_t moment, OptionalText_t timezone); +Int_t Moment$day_of_month(Moment_t moment, OptionalText_t timezone); +Int_t Moment$day_of_year(Moment_t moment, OptionalText_t timezone); +Int_t Moment$hour(Moment_t moment, OptionalText_t timezone); +Int_t Moment$minute(Moment_t moment, OptionalText_t timezone); +Int_t Moment$second(Moment_t moment, OptionalText_t timezone); +Int_t Moment$nanosecond(Moment_t moment, OptionalText_t timezone); +Text_t Moment$format(Moment_t moment, Text_t fmt, OptionalText_t timezone); +Text_t Moment$date(Moment_t moment, OptionalText_t timezone); +Text_t Moment$time(Moment_t moment, bool seconds, bool am_pm, OptionalText_t timezone); +OptionalMoment_t Moment$parse(Text_t text, Text_t format); +Text_t Moment$relative(Moment_t moment, Moment_t relative_to, OptionalText_t timezone); +CONSTFUNC Int64_t Moment$unix_timestamp(Moment_t moment); +CONSTFUNC Moment_t Moment$from_unix_timestamp(Int64_t timestamp); +void Moment$set_local_timezone(OptionalText_t timezone); +Text_t Moment$get_local_timezone(void); + +extern const TypeInfo_t Moment$info; + +// vim: ts=4 sw=0 et cino=L2,l1,(0,W4,m1,\:0 + diff --git a/stdlib/optionals.c b/stdlib/optionals.c index 7224c421..34f14f4b 100644 --- a/stdlib/optionals.c +++ b/stdlib/optionals.c @@ -5,9 +5,9 @@ #include "bools.h" #include "bytes.h" #include "datatypes.h" -#include "datetime.h" #include "integers.h" #include "metamethods.h" +#include "moments.h" #include "patterns.h" #include "text.h" #include "threads.h" @@ -33,8 +33,8 @@ public PUREFUNC bool is_null(const void *obj, const TypeInfo_t *non_optional_typ return ((OptionalByte_t*)obj)->is_null; else if (non_optional_type == &Thread$info) return *(pthread_t**)obj == NULL; - else if (non_optional_type == &DateTime$info) - return ((OptionalDateTime_t*)obj)->tv_usec < 0; + else if (non_optional_type == &Moment$info) + return ((OptionalMoment_t*)obj)->tv_usec < 0; else if (non_optional_type == &Match$info) return ((OptionalMatch_t*)obj)->index.small == 0; diff --git a/stdlib/optionals.h b/stdlib/optionals.h index 2db8bdea..b5326479 100644 --- a/stdlib/optionals.h +++ b/stdlib/optionals.h @@ -22,7 +22,7 @@ #define NULL_TABLE ((OptionalTable_t){.entries.length=-1}) #define NULL_CLOSURE ((OptionalClosure_t){.fn=NULL}) #define NULL_TEXT ((OptionalText_t){.length=-1}) -#define NULL_DATETIME ((OptionalDateTime_t){.tv_usec=-1}) +#define NULL_MOMENT ((OptionalMoment_t){.tv_usec=-1}) PUREFUNC bool is_null(const void *obj, const TypeInfo_t *non_optional_type); Text_t Optional$as_text(const void *obj, bool colorize, const TypeInfo_t *type); diff --git a/stdlib/paths.c b/stdlib/paths.c index 6e138b57..56492718 100644 --- a/stdlib/paths.c +++ b/stdlib/paths.c @@ -205,28 +205,28 @@ public bool Path$is_symlink(Path_t path) return (sb.st_mode & S_IFMT) == S_IFLNK; } -public OptionalDateTime_t Path$modified(Path_t path, bool follow_symlinks) +public OptionalMoment_t Path$modified(Path_t path, bool follow_symlinks) { struct stat sb; int status = path_stat(path, follow_symlinks, &sb); - if (status != 0) return NULL_DATETIME; - return (DateTime_t){.tv_sec=sb.st_mtime}; + if (status != 0) return NULL_MOMENT; + return (Moment_t){.tv_sec=sb.st_mtime}; } -public OptionalDateTime_t Path$accessed(Path_t path, bool follow_symlinks) +public OptionalMoment_t Path$accessed(Path_t path, bool follow_symlinks) { struct stat sb; int status = path_stat(path, follow_symlinks, &sb); - if (status != 0) return NULL_DATETIME; - return (DateTime_t){.tv_sec=sb.st_atime}; + if (status != 0) return NULL_MOMENT; + return (Moment_t){.tv_sec=sb.st_atime}; } -public OptionalDateTime_t Path$changed(Path_t path, bool follow_symlinks) +public OptionalMoment_t Path$changed(Path_t path, bool follow_symlinks) { struct stat sb; int status = path_stat(path, follow_symlinks, &sb); - if (status != 0) return NULL_DATETIME; - return (DateTime_t){.tv_sec=sb.st_ctime}; + if (status != 0) return NULL_MOMENT; + return (Moment_t){.tv_sec=sb.st_ctime}; } static void _write(Path_t path, Array_t bytes, int mode, int permissions) diff --git a/stdlib/paths.h b/stdlib/paths.h index 989e5d84..bc263957 100644 --- a/stdlib/paths.h +++ b/stdlib/paths.h @@ -27,9 +27,9 @@ bool Path$is_directory(Path_t path, bool follow_symlinks); bool Path$is_pipe(Path_t path, bool follow_symlinks); bool Path$is_socket(Path_t path, bool follow_symlinks); bool Path$is_symlink(Path_t path); -OptionalDateTime_t Path$modified(Path_t path, bool follow_symlinks); -OptionalDateTime_t Path$accessed(Path_t path, bool follow_symlinks); -OptionalDateTime_t Path$changed(Path_t path, bool follow_symlinks); +OptionalMoment_t Path$modified(Path_t path, bool follow_symlinks); +OptionalMoment_t Path$accessed(Path_t path, bool follow_symlinks); +OptionalMoment_t Path$changed(Path_t path, bool follow_symlinks); void Path$write(Path_t path, Text_t text, int permissions); void Path$write_bytes(Path_t path, Array_t bytes, int permissions); void Path$append(Path_t path, Text_t text, int permissions); diff --git a/stdlib/tomo.h b/stdlib/tomo.h index 8b378c0a..159b6426 100644 --- a/stdlib/tomo.h +++ b/stdlib/tomo.h @@ -13,11 +13,11 @@ #include "c_strings.h" #include "channels.h" #include "datatypes.h" -#include "datetime.h" #include "functiontype.h" #include "integers.h" #include "memory.h" #include "metamethods.h" +#include "moments.h" #include "nums.h" #include "optionals.h" #include "paths.h" diff --git a/test/datetime.tm b/test/datetime.tm deleted file mode 100644 index 64077c27..00000000 --- a/test/datetime.tm +++ /dev/null @@ -1,49 +0,0 @@ - -func main(): - >> 2024-1-1 12:00[America/New_York] == 2024-1-1T09:00[America/Los_Angeles] - = yes - >> 2024-1-1 12:00[America/New_York] == DateTime(2024, 1, 1, hour=9, timezone="America/Los_Angeles") - = yes - - >> t := 2024-1-2 13:45[America/New_York] - >> t:after(days=40) == 2024-2-11T13:45:00[America/New_York] - = yes - >> t:date(timezone="America/New_York") - = "2024-01-02" - - >> t:time(timezone="America/New_York") - = "1:45pm" - - >> t:time(am_pm=no, timezone="America/New_York") - = "13:45" - - >> t:relative(relative_to=t:after(minutes=65)) - = "1 hour ago" - - >> t:seconds_till(t:after(minutes=2)) - = 120 - - >> t:minutes_till(t:after(minutes=2)) - = 2 - - >> t:hours_till(t:after(minutes=60)) - = 1 - - >> t:day_of_week() # 1 = Sun, 2 = Mon, 3 = Tue - = 3 - - >> t:format("%A") - = "Tuesday" - - >> t:unix_timestamp() - = Int64(1704221100) - >> t == DateTime.from_unix_timestamp(1704221100) - = yes - - >> t < t:after(minutes=1) - = yes - - >> t < t:after(seconds=0.1) - = yes - - >> now() diff --git a/test/moments.tm b/test/moments.tm new file mode 100644 index 00000000..085237ca --- /dev/null +++ b/test/moments.tm @@ -0,0 +1,49 @@ + +func main(): + >> 2024-1-1 12:00[America/New_York] == 2024-1-1T09:00[America/Los_Angeles] + = yes + >> 2024-1-1 12:00[America/New_York] == Moment(2024, 1, 1, hour=9, timezone="America/Los_Angeles") + = yes + + >> t := 2024-1-2 13:45[America/New_York] + >> t:after(days=40) == 2024-2-11T13:45:00[America/New_York] + = yes + >> t:date(timezone="America/New_York") + = "2024-01-02" + + >> t:time(timezone="America/New_York") + = "1:45pm" + + >> t:time(am_pm=no, timezone="America/New_York") + = "13:45" + + >> t:relative(relative_to=t:after(minutes=65)) + = "1 hour ago" + + >> t:seconds_till(t:after(minutes=2)) + = 120 + + >> t:minutes_till(t:after(minutes=2)) + = 2 + + >> t:hours_till(t:after(minutes=60)) + = 1 + + >> t:day_of_week() # 1 = Sun, 2 = Mon, 3 = Tue + = 3 + + >> t:format("%A") + = "Tuesday" + + >> t:unix_timestamp() + = Int64(1704221100) + >> t == Moment.from_unix_timestamp(1704221100) + = yes + + >> t < t:after(minutes=1) + = yes + + >> t < t:after(seconds=0.1) + = yes + + >> now() diff --git a/typecheck.c b/typecheck.c index 915e2b5c..b3d96d37 100644 --- a/typecheck.c +++ b/typecheck.c @@ -704,7 +704,7 @@ type_t *get_type(env_t *env, ast_t *ast) if (fn_type_t->tag == TypeInfoType) { type_t *t = Match(fn_type_t, TypeInfoType)->type; if (t->tag == StructType || t->tag == IntType || t->tag == BigIntType || t->tag == NumType - || t->tag == ByteType || t->tag == TextType || t->tag == CStringType || t->tag == DateTimeType) + || t->tag == ByteType || t->tag == TextType || t->tag == CStringType || t->tag == MomentType) return t; // Constructor code_err(call->fn, "This is not a type that has a constructor"); } @@ -1204,7 +1204,7 @@ type_t *get_type(env_t *env, ast_t *ast) type_ast_t *type_ast = inline_code->type_ast; return type_ast ? parse_type_ast(env, type_ast) : Type(VoidType); } - case DateTime: return Type(DateTimeType); + case Moment: return Type(MomentType); case Unknown: code_err(ast, "I can't figure out the type of: %W", ast); } #pragma GCC diagnostic pop diff --git a/types.c b/types.c index 8fcff830..e03bfb6e 100644 --- a/types.c +++ b/types.c @@ -25,7 +25,7 @@ CORD type_to_cord(type_t *t) { case BoolType: return "Bool"; case ByteType: return "Byte"; case CStringType: return "CString"; - case DateTimeType: return "DateTime"; + case MomentType: return "Moment"; case TextType: return Match(t, TextType)->lang ? Match(t, TextType)->lang : "Text"; case BigIntType: return "Int"; case IntType: return CORD_asprintf("Int%d", Match(t, IntType)->bits); @@ -425,7 +425,7 @@ PUREFUNC size_t type_size(type_t *t) case BoolType: return sizeof(bool); case ByteType: return sizeof(uint8_t); case CStringType: return sizeof(char*); - case DateTimeType: return sizeof(DateTime_t); + case MomentType: return sizeof(Moment_t); case BigIntType: return sizeof(Int_t); case IntType: { switch (Match(t, IntType)->bits) { @@ -509,7 +509,7 @@ PUREFUNC size_t type_align(type_t *t) case BoolType: return __alignof__(bool); case ByteType: return __alignof__(uint8_t); case CStringType: return __alignof__(char*); - case DateTimeType: return __alignof__(DateTime_t); + case MomentType: return __alignof__(Moment_t); case BigIntType: return __alignof__(Int_t); case IntType: { switch (Match(t, IntType)->bits) { diff --git a/types.h b/types.h index c983dc24..aa0caf49 100644 --- a/types.h +++ b/types.h @@ -48,7 +48,7 @@ struct type_s { IntType, NumType, CStringType, - DateTimeType, + MomentType, TextType, ArrayType, ChannelType, @@ -78,7 +78,7 @@ struct type_s { struct { enum { TYPE_NBITS32=32, TYPE_NBITS64=64 } bits; } NumType; - struct {} CStringType, DateTimeType; + struct {} CStringType, MomentType; struct { const char *lang; struct env_s *env; -- cgit v1.2.3