aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile2
-rw-r--r--compile.c9
-rw-r--r--docs/datetime.md435
-rw-r--r--environment.c27
-rw-r--r--stdlib/datatypes.h8
-rw-r--r--stdlib/datetime.c216
-rw-r--r--stdlib/datetime.h32
-rw-r--r--stdlib/optionals.c5
-rw-r--r--stdlib/optionals.h1
-rw-r--r--stdlib/paths.c51
-rw-r--r--stdlib/paths.h3
-rw-r--r--stdlib/tomo.h1
-rw-r--r--types.c3
-rw-r--r--types.h3
14 files changed, 771 insertions, 25 deletions
diff --git a/Makefile b/Makefile
index 3b21ebad..2508d891 100644
--- a/Makefile
+++ b/Makefile
@@ -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
diff --git a/compile.c b/compile.c
index b554afc9..328dfdf6 100644
--- a/compile.c
+++ b/compile.c
@@ -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"
diff --git a/types.c b/types.c
index c1862e07..024e4289 100644
--- a/types.c
+++ b/types.c
@@ -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) {
diff --git a/types.h b/types.h
index d92e217d..a1f7cae1 100644
--- a/types.h
+++ b/types.h
@@ -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;