code / tomo-time

Lines215 Tomo183 C21 Markdown11
(227 lines)
1 # Time - a module for dealing with dates and times
2 use <math.h>
3 use ./time_defs.h
5 enum Weekday(Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday)
7 struct TimeInfo(year,month,day,hour,minute,second,nanosecond:Int, weekday:Weekday, day_of_year:Int, timezone:Text)
9 func _num_format(n:Num, unit:CString -> Text)
10 if n == 0
11 return "now"
12 return "$(Int(n, truncate=yes)) $(unit)$((if n == 1 or n == -1 then "" else "s")) $((if n < 0 then "ago" else "from now"))"
14 struct Time(seconds:Int64, nanoseconds:Int64)
15 func now(->Time)
16 time : Time
17 C_code `
18 struct timespec ts;
19 if (clock_gettime(CLOCK_REALTIME, &ts) != 0)
20 fail("Couldn't get the time!");
21 @(time.seconds) = ts.tv_sec;
22 @(time.nanoseconds) = ts.tv_nsec;
24 return time
26 func local_timezone(->Text)
27 C_code `
28 if (_local_timezone.tag == TEXT_NONE) {
29 static char buf[PATH_MAX];
30 ssize_t len = readlink("/etc/localtime", buf, sizeof(buf));
31 if (len < 0)
32 fail("Could not get local tz!");
34 char *zoneinfo = strstr(buf, "/zoneinfo/");
35 if (zoneinfo)
36 _local_timezone = Text$from_str(zoneinfo + strlen("/zoneinfo/"));
37 else
38 fail("Could not resolve local tz!");
41 return C_code:Text`_local_timezone`
43 func set_local_timezone(timezone:Text)
44 C_code `
45 setenv("TZ", @(CString(timezone)), 1);
46 _local_timezone = @timezone;
47 tzset();
50 func format(t:Time, format="%c", timezone=Time.local_timezone() -> Text)
51 return C_code : Text `
52 struct tm result;
53 time_t time = @(t.seconds);
54 struct tm *final_info;
55 WITH_TIMEZONE(@timezone, final_info = localtime_r(&time, &result));
56 static char buf[256];
57 size_t len = strftime(buf, sizeof(buf), String(@format), final_info);
58 Text$from_strn(buf, len)
61 func new(year,month,day:Int, hour=0, minute=0, second=0.0, timezone=Time.local_timezone() -> Time)
62 time : Time
63 C_code `
64 struct tm info = {
65 .tm_min=Int32$from_int(@minute, false),
66 .tm_hour=Int32$from_int(@hour, false),
67 .tm_mday=Int32$from_int(@day, false),
68 .tm_mon=Int32$from_int(@month, false) - 1,
69 .tm_year=Int32$from_int(@year, false) - 1900,
70 .tm_isdst=-1,
71 };
73 time_t t;
74 WITH_TIMEZONE(@timezone, t = mktime(&info));
75 @(time.seconds) = t + (int64_t)@second;
76 @(time.nanoseconds) = (int64_t)(fmod(@second, 1.0) * 1e9);
78 return time
80 func unix_timestamp(t:Time -> Int64)
81 return t.seconds
83 func from_unix_timestamp(timestamp:Int64 -> Time)
84 return Time(seconds=timestamp, nanoseconds=0)
86 func seconds_till(t:Time, target:Time -> Num)
87 seconds := Num(target.seconds - t.seconds)
88 seconds += 1e-9*Num(target.nanoseconds - t.nanoseconds)
89 return seconds
91 func minutes_till(t:Time, target:Time -> Num)
92 return t.seconds_till(target)/60.
94 func hours_till(t:Time, target:Time -> Num)
95 return t.seconds_till(target)/3600.
97 func relative(t:Time, relative_to=Time.now(), timezone=Time.local_timezone() -> Text)
98 C_code `
99 struct tm info = {};
100 struct tm relative_info = {};
101 WITH_TIMEZONE(@timezone, {
102 localtime_r(&@(t.seconds), &info);
103 localtime_r(&@(relative_to.seconds), &relative_info);
104 });
105 double second_diff = @(relative_to.seconds_till(t));
106 if (info.tm_year != relative_info.tm_year && fabs(second_diff) > 365.*24.*60.*60.)
107 return @_num_format((long)info.tm_year - (long)relative_info.tm_year, "year");
108 else if (info.tm_mon != relative_info.tm_mon && fabs(second_diff) > 31.*24.*60.*60.)
109 return @_num_format(12*((long)info.tm_year - (long)relative_info.tm_year) + (long)info.tm_mon - (long)relative_info.tm_mon, "month");
110 else if (info.tm_yday != relative_info.tm_yday && fabs(second_diff) > 24.*60.*60.)
111 return @_num_format(round(second_diff/(24.*60.*60.)), "day");
112 else if (info.tm_hour != relative_info.tm_hour && fabs(second_diff) > 60.*60.)
113 return @_num_format(round(second_diff/(60.*60.)), "hour");
114 else if (info.tm_min != relative_info.tm_min && fabs(second_diff) > 60.)
115 return @_num_format(round(second_diff/(60.)), "minute");
116 else {
117 if (fabs(second_diff) < 1e-6)
118 return @_num_format((long)(second_diff*1e9), "nanosecond");
119 else if (fabs(second_diff) < 1e-3)
120 return @_num_format((long)(second_diff*1e6), "microsecond");
121 else if (fabs(second_diff) < 1.0)
122 return @_num_format((long)(second_diff*1e3), "millisecond");
123 else
124 return @_num_format((long)(second_diff), "second");
127 fail("Unreachable")
129 func time(t:Time, seconds=no, am_pm=yes, timezone=Time.local_timezone() -> Text)
130 time := if seconds and am_pm
131 t.format("%l:%M:%S%P")
132 else if seconds and not am_pm
133 t.format("%T")
134 else if not seconds and am_pm
135 t.format("%l:%M%P")
136 else
137 t.format("%H:%M")
138 return time.trim()
140 func date(t:Time, timezone=Time.local_timezone() -> Text)
141 return t.format("%F")
143 func info(t:Time, timezone=Time.local_timezone() -> TimeInfo)
144 ret : TimeInfo
145 C_code `
146 struct tm info = {};
147 WITH_TIMEZONE(@timezone, localtime_r(&@(t.seconds), &info));
148 @(ret.year) = I(info.tm_year + 1900);
149 @(ret.month) = I(info.tm_mon + 1);
150 @(ret.day) = I(info.tm_mday);
151 @(ret.hour) = I(info.tm_hour);
152 @(ret.minute) = I(info.tm_min);
153 @(ret.second) = I(info.tm_sec);
154 @(ret.nanosecond) = I(@(t.nanoseconds));
155 @(ret.weekday).$tag = info.tm_wday + 1;
156 @(ret.day_of_year) = I(info.tm_yday);
158 ret.timezone = timezone
159 return ret
161 func after(t:Time, seconds=0.0, minutes=0.0, hours=0.0, days=0, weeks=0, months=0, years=0, timezone=Time.local_timezone() -> Time)
162 ret : Time
163 C_code `
164 double offset = @seconds + 60.*@minutes + 3600.*@hours ;
165 @(t.seconds) += (time_t)offset;
167 struct tm info = {};
168 WITH_TIMEZONE(@timezone, localtime_r(&@(t.seconds), &info));
170 info.tm_mday += Int32$from_int(@days, false) + 7*Int32$from_int(@weeks, false);
171 info.tm_mon += Int32$from_int(@months, false);
172 info.tm_year += Int32$from_int(@years, false);
174 time_t t = mktime(&info);
175 @(ret.seconds) = t;
176 @(ret.nanoseconds) = @(t.nanoseconds) + (int64_t)(fmod(offset, 1.0) * 1e9);
178 return ret
180 func parse(text:Text, format="%Y-%m-%dT%H:%M:%S%z", timezone=Time.local_timezone() -> Time?)
181 ret : Time? = none
182 C_code `
183 struct tm info = {.tm_isdst=-1};
184 const char *str = Text$as_c_string(@text);
185 const char *fmt = Text$as_c_string(@format);
186 if (strstr(fmt, "%Z"))
187 fail("The %Z specifier is not supported for time parsing!");
189 char *invalid = NULL;
190 WITH_TIMEZONE(@timezone, invalid = strptime(str, fmt, &info));
191 if (invalid != NULL && invalid[0] == '\0') {
192 long offset = info.tm_gmtoff; // Need to cache this because mktime() mutates it to local tz >:(
193 time_t t;
194 WITH_TIMEZONE(@timezone, t = mktime(&info));
195 @ret.value.seconds = t + offset - info.tm_gmtoff;
198 return ret
200 func now(->Time)
201 return Time.now()
203 func _run_tests()
204 >> Time.local_timezone()
205 >> Time.now().format()
206 >> Time.set_local_timezone("Europe/Paris")
207 >> Time.now().format()
208 >> Time.set_local_timezone("America/New_York")
209 >> Time.now().format()
210 # >> Time.now().format(timezone="Europe/Paris")
211 # >> Time.now().format()
212 # >> Time.now().format("%Y-%m-%d")
213 # >> Time.new(2023, 11, 5).format()
214 # >> Time.local_timezone()
216 # >> Time.new(2023, 11, 5).seconds_till(Time.now())
217 # >> Time.new(2023, 11, 5).relative()
219 # >> Time.now().info()
220 # >> Time.now().time()
221 # >> Time.now().date()
223 # >> Time.parse("2023-11-05 01:01", "%Y-%m-%d %H:%M")
224 # >> Time.parse("2023-11-05 01:01", "%Y-%m-%d %H:%M", timezone="Europe/Paris")
226 func main()
227 _run_tests()