diff options
| author | Bruce Hill <bruce@bruce-hill.com> | 2024-09-29 20:06:09 -0400 |
|---|---|---|
| committer | Bruce Hill <bruce@bruce-hill.com> | 2024-09-29 20:06:09 -0400 |
| commit | 05515d8645326efa3db6311f6be1be776452c68a (patch) | |
| tree | 15e58e72fc1aeec61017f02d8bf555e38a8dfda3 | |
| parent | 8f7f66d7c894544c7c7e8ffae5d74e3cc602256f (diff) | |
Add DateTime
| -rw-r--r-- | Makefile | 2 | ||||
| -rw-r--r-- | compile.c | 9 | ||||
| -rw-r--r-- | docs/datetime.md | 435 | ||||
| -rw-r--r-- | environment.c | 27 | ||||
| -rw-r--r-- | stdlib/datatypes.h | 8 | ||||
| -rw-r--r-- | stdlib/datetime.c | 216 | ||||
| -rw-r--r-- | stdlib/datetime.h | 32 | ||||
| -rw-r--r-- | stdlib/optionals.c | 5 | ||||
| -rw-r--r-- | stdlib/optionals.h | 1 | ||||
| -rw-r--r-- | stdlib/paths.c | 51 | ||||
| -rw-r--r-- | stdlib/paths.h | 3 | ||||
| -rw-r--r-- | stdlib/tomo.h | 1 | ||||
| -rw-r--r-- | types.c | 3 | ||||
| -rw-r--r-- | types.h | 3 |
14 files changed, 771 insertions, 25 deletions
@@ -31,7 +31,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/optionals.o stdlib/patterns.o stdlib/metamethods.o stdlib/functiontype.o stdlib/stdlib.o + stdlib/optionals.o stdlib/patterns.o stdlib/metamethods.o stdlib/functiontype.o stdlib/stdlib.o stdlib/datetime.o TESTS=$(patsubst %.tm,%.tm.testresult,$(wildcard test/*.tm)) all: libtomo.so tomo @@ -211,6 +211,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 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); @@ -257,7 +258,7 @@ CORD compile_type(type_t *t) case PointerType: case EnumType: case ChannelType: return compile_type(nonnull); case IntType: case BigIntType: case TextType: case NumType: case BoolType: case ByteType: - case ArrayType: case TableType: case SetType: + case ArrayType: case TableType: case SetType: case DateTimeType: return CORD_all("Optional", compile_type(nonnull)); case StructType: { if (nonnull == THREAD_TYPE) @@ -384,6 +385,8 @@ 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) + return CORD_all("((", value, ").tv_usec < 0)"); errx(1, "Optional check not implemented for: %T", t); } @@ -1443,6 +1446,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 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); @@ -1808,6 +1812,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 PointerType: return CORD_all("((", compile_type(t), ")NULL)"); case ClosureType: return "NULL_CLOSURE"; case NumType: return "nan(\"null\")"; @@ -3397,7 +3402,7 @@ CORD compile_type_info(env_t *env, type_t *t) else if (t == RANGE_TYPE) return "&Range$info"; switch (t->tag) { - case BoolType: case ByteType: case IntType: case BigIntType: case NumType: case CStringType: + case BoolType: case ByteType: case IntType: case BigIntType: case NumType: case CStringType: case DateTimeType: return CORD_all("&", type_to_cord(t), "$info"); case TextType: { auto text = Match(t, TextType); diff --git a/docs/datetime.md b/docs/datetime.md new file mode 100644 index 00000000..24215862 --- /dev/null +++ b/docs/datetime.md @@ -0,0 +1,435 @@ +# 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 +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()`). + +## 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 +``` + +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 functions where this matters, there +is an extra `local_time` argument that is `yes` by default. + +## 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, +`local_time` which is used to determine whether time offsets should be +calculated using the current local time or UTC. + +**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 +``` + +**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. + +**Returns:** +A new `DateTime` offset by the given amount. + +**Example:** +```markdown +>> DateTime.new(2024, 9, 29, hour=19):after(days=1, minutes=30) += Mon Sep 30 19:30:00 2024 +``` + +--- + +### `date` + +**Description:** +Return a text representation of the datetime using the `"%F"` format +specifier, which gives the date in `YYYY-MM-DD` form. + +**Usage:** +```markdown +datetime:date(local_time : Bool = yes) -> Text +``` + +**Parameters:** + +- `local_time`: Whether to use local time (default: `yes`) or UTC. + +**Returns:** +The date in `YYYY-MM-DD` format. + +**Example:** +```markdown +>> DateTime.new(2024, 9, 29):format("%A") += "2024-09-29" +``` + +--- + +### `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. + +**Usage:** +```markdown +datetime:format(format: Text = "%c", local_time : Bool = yes) -> Text +``` + +**Parameters:** + +- `path`: The path of the file to append to. +- `bytes`: The bytes to append to the file. +- `permissions` (optional): The permissions to set on the file if it is being created (default is `0o644`). + +**Returns:** +Nothing. + +**Example:** +```markdown +>> DateTime.new(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). + +**Usage:** +```markdown +DateTime.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:** +```markdown +# In the New York timezone: +>> DateTime.from_unix_timestamp(0) += Wed Dec 31 19:00:00 1969 +``` + +--- + +### `get` + +**Description:** +Get various components of the given datetime object and store them in the +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 +``` + +**Parameters:** + +- `year`: If non-null, store the year here. +- `month`: If non-null, store the month here (1-12). +- `day`: If non-null, store the day of the month here (1-31). +- `hour`: If non-null, store the hour of the day here (0-23). +- `minute`: If non-null, store the minute of the hour here (0-59). +- `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. + +**Returns:** +Nothing. + +**Example:** +```markdown +dt := DateTime.new(2024, 9, 29) +month := 0 +dt:get(month=&month) +>> month += 9 +``` + +--- + +### `hours_till` + +**Description:** +Return the number of hours until a given datetime. + +**Usage:** +```markdown +datetime:hours_till(then:DateTime) -> Num +``` + +**Parameters:** + +- `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:** +```markdown +the_future := now():after(hours=1, minutes=30) +>> now():hours_till(the_future) += 1.5 +``` + +--- + +### `minutes_till` + +**Description:** +Return the number of minutes until a given datetime. + +**Usage:** +```markdown +datetime:minutes_till(then:DateTime) -> Num +``` + +**Parameters:** + +- `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:** +```markdown +the_future := now():after(minutes=1, seconds=30) +>> now():minutes_till(the_future) += 1.5 +``` + +--- + +### `new` + +**Description:** +Return a new `DateTime` object representing the given time parameters expressed +in local time. + +**Usage:** +```markdown +DateTime.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:** +```markdown +>> DateTime.new(2024, 9, 29) += Mon Sep 30 00:00:00 2024 +``` + +--- + +### `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. + +**Usage:** +```markdown +DateTime.parse(text: Text, format: Text = "%c") -> 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: `"%c"`). + +**Returns:** +If the text was successfully parsed according to the given format, return a +`DateTime` representing that information. Otherwise, return a null value. + +**Example:** +```markdown +>> DateTime.parse("2024-09-29", "%Y-%m-%d")! += Sun Sep 29 00:00:00 2024 + +>> 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` + +**Usage:** +```markdown +datetime:relative(relative_to : DateTime = DateTime.now(), local_time : Bool = yes) -> 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`). + +**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:** +```markdown +>> now():after(days=2):relative() += "2 days later" + +>> now():after(minutes=-65):relative() += "1 hour ago" +``` + +--- + +### `seconds_till` + +**Description:** +Return the number of seconds until a given datetime. + +**Usage:** +```markdown +datetime:seconds_till(then:DateTime) -> Num +``` + +**Parameters:** + +- `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:** +```markdown +the_future := now():after(seconds=1) +>> now():seconds_till(the_future) += 1 +``` + +--- + +### `time` + +**Description:** +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 +``` + +**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. + +**Returns:** +A text representation of the time component of the datetime. + +**Example:** +```markdown +dt := DateTime.new(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). + +**Usage:** +```markdown +datetime:unix_timestamp() -> Int64 +``` + +**Parameters:** + +None. + +**Returns:** +A 64-bit integer representation of the UNIX timestamp. + +**Example:** +```markdown +>> now():unix_timestamp() += 1727654730[64] +``` diff --git a/environment.c b/environment.c index 4c261438..b9cbaa47 100644 --- a/environment.c +++ b/environment.c @@ -50,6 +50,7 @@ env_t *new_compilation_unit(CORD libname) .default_val=FakeAST(Int, .bits=IBITS32, .str="1"))), .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))}}, {"USE_COLOR", {.code="USE_COLOR", .type=Type(BoolType)}}, }; @@ -77,7 +78,6 @@ env_t *new_compilation_unit(CORD libname) THREAD_TYPE = Type(StructType, .name="Thread", .env=thread_env, .opaque=true); } - struct { const char *name; type_t *type; @@ -261,6 +261,24 @@ 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, + // 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=\"%c\",local_time=yes)->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)"}, + {"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"}, + {"parse", "DateTime$parse", "func(text:Text, format=\"%c\")->DateTime?"}, + {"relative", "DateTime$relative", "func(dt:DateTime,relative_to=DateTime.now(),local_time=yes)->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"}, + {"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, {"append", "Path$append", "func(path:Path, text:Text, permissions=0o644[32])"}, {"append_bytes", "Path$append_bytes", "func(path:Path, bytes:[Byte], permissions=0o644[32])"}, @@ -292,6 +310,10 @@ 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?"}, + // Text methods: {"ends_with", "Text$ends_with", "func(path:Path, suffix:Text)->Bool"}, {"has", "Text$has", "func(path:Path, pattern:Pattern)->Bool"}, @@ -380,7 +402,6 @@ env_t *new_compilation_unit(CORD libname) } } - set_binding(namespace_env(env, "Shell"), "without_escaping", new(binding_t, .type=Type(FunctionType, .args=new(arg_t, .name="text", .type=TEXT_TYPE), .ret=Type(TextType, .lang="Shell", .env=namespace_env(env, "Shell"))), @@ -579,7 +600,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 CStringType: case DateTimeType: case BoolType: case IntType: case BigIntType: case NumType: { binding_t *b = get_binding(env, CORD_to_const_char_star(type_to_cord(cls_type))); assert(b); diff --git a/stdlib/datatypes.h b/stdlib/datatypes.h index 4bb6beb3..8d342d2e 100644 --- a/stdlib/datatypes.h +++ b/stdlib/datatypes.h @@ -3,9 +3,10 @@ // Common datastructures (arrays, tables, closures) #include <gmp.h> -#include <stdint.h> -#include <stdbool.h> #include <pthread.h> +#include <stdbool.h> +#include <stdint.h> +#include <time.h> #define ARRAY_LENGTH_BITS 42 #define ARRAY_FREE_BITS 6 @@ -89,4 +90,7 @@ typedef struct Text_s { #define Pattern_t Text_t #define OptionalPattern_t Text_t +typedef struct timeval DateTime_t; +#define OptionalDateTime_t DateTime_t + // vim: ts=4 sw=0 et cino=L2,l1,(0,W4,m1,\:0 diff --git a/stdlib/datetime.c b/stdlib/datetime.c new file mode 100644 index 00000000..a3bb42c1 --- /dev/null +++ b/stdlib/datetime.c @@ -0,0 +1,216 @@ +// DateTime methods/type info +#include <ctype.h> +#include <gc.h> +#include <err.h> +#include <stdlib.h> +#include <time.h> + +#include "datatypes.h" +#include "optionals.h" +#include "patterns.h" +#include "stdlib.h" +#include "text.h" +#include "util.h" + +public Text_t DateTime$as_text(const DateTime_t *dt, bool colorize, const TypeInfo *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", 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 *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) +{ + 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 = 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, bool local_time) +{ + double offset = seconds + 60.*minutes + 3600.*hours; + dt.tv_sec += (time_t)offset; + + struct tm info = {}; + if (local_time) + localtime_r(&dt.tv_sec, &info); + else + gmtime_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, bool local_time) +{ + struct tm info = {}; + if (local_time) + localtime_r(&dt.tv_sec, &info); + else + gmtime_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 Text_t DateTime$format(DateTime_t dt, Text_t fmt, bool local_time) +{ + struct tm info; + struct tm *final_info = local_time ? localtime_r(&dt.tv_sec, &info) : gmtime_r(&dt.tv_sec, &info); + static char buf[256]; + size_t len = strftime(buf, sizeof(buf), Text$as_c_string(fmt), final_info); + return Text$format("%.*s", (int)len, buf); +} + +public Text_t DateTime$date(DateTime_t dt, bool local_time) +{ + return DateTime$format(dt, Text("%F"), local_time); +} + +public Text_t DateTime$time(DateTime_t dt, bool seconds, bool am_pm, bool local_time) +{ + Text_t text; + if (seconds) + text = DateTime$format(dt, am_pm ? Text("%l:%M:%S%P") : Text("%T"), local_time); + else + text = DateTime$format(dt, am_pm ? Text("%l:%M%P") : Text("%H:%M"), local_time); + return Text$trim(text, Pattern(" "), true, true); +} + +public OptionalDateTime_t DateTime$parse(Text_t text, Text_t format) +{ + struct tm info = {.tm_isdst=-1}; + char *invalid = strptime(Text$as_c_string(text), Text$as_c_string(format), &info); + if (!invalid || invalid[0] != '\0') + return NULL_DATETIME; + + time_t t = mktime(&info); + return (DateTime_t){.tv_sec=t}; +} + +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, bool local_time) +{ + 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) + localtime_r(&relative_to.tv_sec, &relative_info); + else + gmtime_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 const TypeInfo 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 new file mode 100644 index 00000000..6f9c7fb0 --- /dev/null +++ b/stdlib/datetime.h @@ -0,0 +1,32 @@ +#pragma once + +// DateTime objects + +#include <stdint.h> + +#include "datatypes.h" +#include "integers.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); +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); +OptionalDateTime_t DateTime$parse(Text_t text, Text_t format); +Text_t DateTime$relative(DateTime_t dt, DateTime_t relative_to, bool local_time); +CONSTFUNC Int64_t DateTime$unix_timestamp(DateTime_t dt); +CONSTFUNC DateTime_t DateTime$from_unix_timestamp(Int64_t timestamp); + +extern const TypeInfo DateTime$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 717a1e0c..15f93846 100644 --- a/stdlib/optionals.c +++ b/stdlib/optionals.c @@ -5,10 +5,11 @@ #include "bools.h" #include "bytes.h" #include "datatypes.h" +#include "datetime.h" #include "integers.h" #include "metamethods.h" -#include "threads.h" #include "text.h" +#include "threads.h" #include "util.h" public PUREFUNC bool is_null(const void *obj, const TypeInfo *non_optional_type) @@ -31,6 +32,8 @@ public PUREFUNC bool is_null(const void *obj, const TypeInfo *non_optional_type) 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; switch (non_optional_type->tag) { case ChannelInfo: return *(Channel_t**)obj == NULL; diff --git a/stdlib/optionals.h b/stdlib/optionals.h index 31fad710..563b5c30 100644 --- a/stdlib/optionals.h +++ b/stdlib/optionals.h @@ -22,6 +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}) PUREFUNC bool is_null(const void *obj, const TypeInfo *non_optional_type); Text_t Optional$as_text(const void *obj, bool colorize, const TypeInfo *type); diff --git a/stdlib/paths.c b/stdlib/paths.c index 0119dbf0..0519201d 100644 --- a/stdlib/paths.c +++ b/stdlib/paths.c @@ -157,56 +157,77 @@ public bool Path$exists(Path_t path) return (stat(Text$as_c_string(path), &sb) == 0); } -public bool Path$is_file(Path_t path, bool follow_symlinks) +static inline int path_stat(Path_t path, bool follow_symlinks, struct stat *sb) { path = Path$_expand_home(path); - struct stat sb; const char *path_str = Text$as_c_string(path); - int status = follow_symlinks ? stat(path_str, &sb) : lstat(path_str, &sb); + return follow_symlinks ? stat(path_str, sb) : lstat(path_str, sb); +} + +public bool Path$is_file(Path_t path, bool follow_symlinks) +{ + struct stat sb; + int status = path_stat(path, follow_symlinks, &sb); if (status != 0) return false; return (sb.st_mode & S_IFMT) == S_IFREG; } public bool Path$is_directory(Path_t path, bool follow_symlinks) { - path = Path$_expand_home(path); struct stat sb; - const char *path_str = Text$as_c_string(path); - int status = follow_symlinks ? stat(path_str, &sb) : lstat(path_str, &sb); + int status = path_stat(path, follow_symlinks, &sb); if (status != 0) return false; return (sb.st_mode & S_IFMT) == S_IFDIR; } public bool Path$is_pipe(Path_t path, bool follow_symlinks) { - path = Path$_expand_home(path); struct stat sb; - const char *path_str = Text$as_c_string(path); - int status = follow_symlinks ? stat(path_str, &sb) : lstat(path_str, &sb); + int status = path_stat(path, follow_symlinks, &sb); if (status != 0) return false; return (sb.st_mode & S_IFMT) == S_IFIFO; } public bool Path$is_socket(Path_t path, bool follow_symlinks) { - path = Path$_expand_home(path); struct stat sb; - const char *path_str = Text$as_c_string(path); - int status = follow_symlinks ? stat(path_str, &sb) : lstat(path_str, &sb); + int status = path_stat(path, follow_symlinks, &sb); if (status != 0) return false; return (sb.st_mode & S_IFMT) == S_IFSOCK; } public bool Path$is_symlink(Path_t path) { - path = Path$_expand_home(path); struct stat sb; - const char *path_str = Text$as_c_string(path); - int status = stat(path_str, &sb); + int status = path_stat(path, false, &sb); if (status != 0) return false; return (sb.st_mode & S_IFMT) == S_IFLNK; } +public OptionalDateTime_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}; +} + +public OptionalDateTime_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}; +} + +public OptionalDateTime_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}; +} + static void _write(Path_t path, Array_t bytes, int mode, int permissions) { path = Path$_expand_home(path); diff --git a/stdlib/paths.h b/stdlib/paths.h index dd6129ea..07ddfb27 100644 --- a/stdlib/paths.h +++ b/stdlib/paths.h @@ -26,6 +26,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); 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 d1e61a5d..515bb8da 100644 --- a/stdlib/tomo.h +++ b/stdlib/tomo.h @@ -15,6 +15,7 @@ #include "c_strings.h" #include "channels.h" #include "datatypes.h" +#include "datetime.h" #include "functiontype.h" #include "integers.h" #include "memory.h" @@ -25,6 +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 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); @@ -405,6 +406,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 BigIntType: return sizeof(Int_t); case IntType: { switch (Match(t, IntType)->bits) { @@ -484,6 +486,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 BigIntType: return __alignof__(Int_t); case IntType: { switch (Match(t, IntType)->bits) { @@ -48,6 +48,7 @@ struct type_s { IntType, NumType, CStringType, + DateTimeType, TextType, ArrayType, ChannelType, @@ -77,7 +78,7 @@ struct type_s { struct { enum { TYPE_NBITS32=32, TYPE_NBITS64=64 } bits; } NumType; - struct {} CStringType; + struct {} CStringType, DateTimeType; struct { const char *lang; struct env_s *env; |
