aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--TECH.md18
-rw-r--r--date.c418
-rw-r--r--project.janet7
-rw-r--r--src/date.h28
-rw-r--r--src/main.c6
-rw-r--r--src/polyfill.c19
-rw-r--r--src/polyfill.h22
-rw-r--r--src/time.c100
-rw-r--r--src/tm.c164
-rw-r--r--src/util.c58
-rw-r--r--test/01-native.janet50
11 files changed, 421 insertions, 469 deletions
diff --git a/TECH.md b/TECH.md
new file mode 100644
index 0000000..f2bbb27
--- /dev/null
+++ b/TECH.md
@@ -0,0 +1,18 @@
+# Time is a mess
+ISO C99 is fairly restrictive, while time in the wild is *wild*.
+
+> The mktime function converts the broken-down time, expressed as local time, in the
+> structure pointed to by timeptr into a calendar time value with the same encoding as
+> that of the values returned by the time function. The original values of the tm_wday
+> and tm_yday components of the structure are ignored, and the original values of the
+> other components are not restricted to the ranges indicated above. On successful
+> completion, the values of the tm_wday and tm_yday components of the structure are
+> set appropriately, and the other components are set to represent the specified calendar
+> time, but with their values forced to the ranges indicated above; the final value of
+> tm_mday is not set until tm_mon and tm_year are determined.
+
+This means that `mktime` must operate on `localtime` output.
+The implication (which is accurate) implies that `gmtime(t) == gmtime(mktime(localtime(t)))`.
+
+As such, the source of truth is `time_t`, which is UTC-only.
+When we want to modify a `time_t`, we want to use localtime to perform the modification.
diff --git a/date.c b/date.c
deleted file mode 100644
index 9250f5d..0000000
--- a/date.c
+++ /dev/null
@@ -1,418 +0,0 @@
-#include <janet.h>
-#include <time.h>
-
-// polyfills
-#if JANET_VERSION_MAJOR < 2 && JANET_VERSION_MINOR < 28
-const char* janet_getcbytes(const Janet *argv, int32_t n) {
- JanetByteView view = janet_getbytes(argv, n);
- const char *cstr = (const char *)view.bytes;
- if (strlen(cstr) != (size_t) view.len) {
- janet_panic("bytes contain embedded 0s");
- }
- return cstr;
-}
-
-const char *janet_optcbytes(const Janet *argv, int32_t argc, int32_t n, const char *dflt) {
- if (n >= argc || janet_checktype(argv[n], JANET_NIL)) {
- return dflt;
- }
- return janet_getcbytes(argv, n);
-}
-#endif
-
-#if !defined(JANET_NO_SOURCEMAPS) && !defined(JANET_NO_DOCSTRINGS)
-#undef JANET_FN
-#define JANET_FN(CNAME, USAGE, DOCSTRING) \
- static const int32_t CNAME##_sourceline_ = __LINE__; \
- static const char CNAME##_docstring_[] = USAGE "\n\n" DOCSTRING; \
- Janet CNAME (int32_t argc, Janet *argv)
-#endif
-
-// forward declarations
-JANET_CFUN(jgmtime);
-JANET_CFUN(jlocaltime);
-JANET_CFUN(jmktime);
-JANET_CFUN(jmktime_inplace);
-JANET_CFUN(jstrftime);
-JANET_CFUN(jtmdict);
-
-// native date/time module that's strictly based on the C specification
-// must build with c99, may opt-into c11+ features
-// we make native time_t and struct tm into abstract types to get free gc and methods
-
-static JanetMethod jtime_methods[] = {
- {"gmtime", jgmtime},
- {"localtime", jlocaltime},
- {NULL, NULL},
-};
-
-static JanetMethod jtm_methods[] = {
- {"mktime", jmktime},
- {"normalize", jmktime_inplace},
- {"strftime", jstrftime},
- {"todict", jtmdict},
- {NULL, NULL},
-};
-
-static int jtime_get(void *p, Janet key, Janet *out) {
- (void) p;
- if (!janet_checktype(key, JANET_KEYWORD)) {
- return 0;
- }
- return janet_getmethod(janet_unwrap_keyword(key), jtime_methods, out);
-}
-
-#define JDATE_KEYEQ(name) if(janet_keyeq(key, #name)) { *out = janet_wrap_integer(tm->tm_##name); return 1; }
-static int jtm_get(void *p, Janet key, Janet *out) {
- struct tm *tm = (struct tm*)p;
- if (!janet_checktype(key, JANET_KEYWORD)) {
- return 0;
- }
-
- // is it a method?
- if(janet_getmethod(janet_unwrap_keyword(key), jtm_methods, out)) {
- return 1;
- }
-
- // is it a tm member?
- JDATE_KEYEQ(sec);
- JDATE_KEYEQ(min);
- JDATE_KEYEQ(hour);
- JDATE_KEYEQ(mday);
- JDATE_KEYEQ(mon);
- // year is defined as years since 1900
- if (janet_keyeq(key, "year")) {
- *out = janet_wrap_integer(tm->tm_year + 1900);
- return 1;
- }
- JDATE_KEYEQ(wday);
- JDATE_KEYEQ(yday);
- if (janet_keyeq(key, "isdst")) {
- if(tm->tm_isdst == 0) {
- *out = janet_wrap_false();
- } else if (tm->tm_isdst > 0) {
- *out = janet_wrap_true();
- } else {
- *out = janet_ckeywordv("detect");
- }
- return 1;
- }
- return 0;
-}
-#undef JDATE_KEYEQ
-
-static inline int compare_time_t(time_t lhs, time_t rhs) {
- // difftime returns the difference in seconds
- return difftime(lhs, rhs);
-}
-
-static int jtime_compare(void *lhs, void *rhs) {
- time_t lhv = (*(time_t*)lhs);
- time_t rhv = (*(time_t*)rhs);
- return compare_time_t(lhv, rhv);
-}
-
-static int jtm_compare(void *lhs, void *rhs) {
- struct tm lhc = *((struct tm*)lhs);
- struct tm rhc = *((struct tm*)rhs);
- time_t lhv = mktime(&lhc);
- time_t rhv = mktime(&rhc);
- return compare_time_t(lhv, rhv);
-}
-
-#define JSTRFTIME_CHUNK 64
-// we expect buffer to be empty on init
-static JanetBuffer* strftime_buffer(const char *format, const struct tm *tm, JanetBuffer *buffer) {
- if(!buffer) buffer = janet_buffer(0);
- size_t offset = buffer->count;
- size_t written = 0;
- do {
- janet_buffer_extra(buffer, JSTRFTIME_CHUNK);
- written = strftime((char*)buffer->data + offset, buffer->capacity - offset,
- format, tm);
- } while (!written);
- buffer->count = written + offset; // does not include \0, which we don't want anyway
- return buffer;
-}
-#undef JSTRFTIME_CHUNK
-
-static void jtm_tostring(void *p, JanetBuffer *buffer) {
- // ISO 8601
- strftime_buffer("%F %T.000", (struct tm*)p, buffer);
-}
-
-static void jtime_tostring(void *p, JanetBuffer *buffer) {
- // ctime is deprecated but lets us know the intended approach is localtime()
- jtm_tostring(localtime(p), buffer);
-}
-
-static const char* keys[] = {
- "sec", "min", "hour", "mday", "mon", "year", "wday", "yday", NULL
-};
-static Janet jtm_next(void *p, Janet key) {
- (void) p;
- const char **ptr = keys;
- while(*ptr) {
- if (janet_keyeq(key, *ptr)) {
- return *(++ptr) ? janet_ckeywordv(*ptr) : janet_wrap_nil();
- }
- ptr++;
- }
- return janet_ckeywordv(keys[0]);
-}
-
-// TODO: hash
-static const JanetAbstractType jtime_type = {
- "time",
- NULL,
- NULL,
- jtime_get,
- NULL,
- NULL,
- NULL,
- jtime_tostring,
- jtime_compare,
- JANET_ATEND_COMPARE
-};
-
-static const JanetAbstractType jtm_type = {
- "tm",
- NULL,
- NULL,
- jtm_get,
- NULL,
- NULL,
- NULL,
- jtm_tostring,
- jtm_compare,
- NULL,
- jtm_next,
- JANET_ATEND_NEXT
-};
-
-static time_t *janet_getjtime(Janet *argv, int32_t n) {
- return (time_t*)janet_getabstract(argv, n, &jtime_type);
-}
-
-static struct tm *janet_getjtm(Janet *argv, int32_t n) {
- return (struct tm*)janet_getabstract(argv, n, &jtm_type);
-}
-
-JANET_FN(jgmtime,
- "",
- "") {
- janet_fixarity(argc, 1);
- time_t *time = janet_getjtime(argv, 0);
- struct tm *tm = janet_abstract(&jtm_type, sizeof(struct tm));
- struct tm *in = gmtime(time);
- *tm = *in;
- return janet_wrap_abstract(tm);
-}
-
-JANET_FN(jlocaltime,
- "",
- "") {
- janet_fixarity(argc, 1);
- time_t *time = janet_getjtime(argv, 0);
- struct tm *tm = janet_abstract(&jtm_type, sizeof(struct tm));
- struct tm *in = localtime(time);
- *tm = *in;
- return janet_wrap_abstract(tm);
-}
-
-// does not mutate input
-JANET_FN(jmktime,
- "",
- "") {
- janet_fixarity(argc, 1);
- struct tm *tm = janet_getjtm(argv, 0);
- struct tm new = *tm;
- time_t *time = janet_abstract(&jtime_type, sizeof(time_t));
- *time = mktime(&new);
- return janet_wrap_abstract(time);
-}
-
-// mutates input
-JANET_FN(jmktime_inplace,
- "",
- "") {
- janet_fixarity(argc, 1);
- struct tm *tm = janet_getjtm(argv, 0);
- time_t *time = janet_abstract(&jtime_type, sizeof(time_t));
- *time = mktime(tm);
- return janet_wrap_abstract(time);
-}
-
-JANET_FN(jtime,
- "",
- "") {
- janet_fixarity(argc, 0);
- time_t *jtime = (time_t*)janet_abstract(&jtime_type, sizeof(time_t));
- time(jtime);
- return janet_wrap_abstract(jtime);
-}
-
-struct strftime_format {
- const char* keyword;
- const char* format;
-};
-const static struct strftime_format builtin_formats[] = {
- {"iso8601", "%F %T.000"},
- {"locale", "%c"},
- // WARN: will not work as you expect if it came from gmtime
- {"rfc5322", "%a, %d %b %Y %T %z"},
- // variant of rfc5322 that is compatible with gmtime and only gmtime
- {"email", "%d %b %Y %T -0000"},
- {"timezone", "%Z"},
- {"tzoffset", "%z"},
- {NULL, NULL}
-};
-
-JANET_FN(jstrftime,
- "",
- "") {
- janet_fixarity(argc, 2);
- // we reverse the order of the function for tm to be the first arg for the method
- struct tm *tm = janet_getjtm(argv, 0);
-
- // determine format
- const char *format = NULL;
- // is it a preset?
- if (janet_checktype(argv[1], JANET_KEYWORD)) {
- const struct strftime_format *ptr = builtin_formats;
- while(ptr->keyword) {
- if (janet_keyeq(argv[1], ptr->keyword)) {
- format = ptr->format;
- break;
- }
- ptr++;
- }
- }
- // either not a preset or not found
- if (!format) format = janet_getcbytes(argv, 1);
-
- return janet_wrap_buffer(strftime_buffer(format, tm, NULL));
-}
-
-// common between dict->tm and dict->time
-static Janet tm_from_vals(JanetDictView dict) {
- struct tm *tm = janet_abstract(&jtm_type, sizeof(struct tm));
- Janet jsec = janet_dictionary_get(dict.kvs, dict.cap, janet_ckeywordv("sec"));
- Janet jmin = janet_dictionary_get(dict.kvs, dict.cap, janet_ckeywordv("min"));
- Janet jhour = janet_dictionary_get(dict.kvs, dict.cap, janet_ckeywordv("hour"));
- Janet jmday = janet_dictionary_get(dict.kvs, dict.cap, janet_ckeywordv("mday"));
- Janet jmon = janet_dictionary_get(dict.kvs, dict.cap, janet_ckeywordv("mon"));
- Janet jyear = janet_dictionary_get(dict.kvs, dict.cap, janet_ckeywordv("year"));
- Janet jwday = janet_dictionary_get(dict.kvs, dict.cap, janet_ckeywordv("wday"));
- Janet jyday = janet_dictionary_get(dict.kvs, dict.cap, janet_ckeywordv("yday"));
- Janet jisdst = janet_dictionary_get(dict.kvs, dict.cap, janet_ckeywordv("isdst"));
-
- tm->tm_sec = janet_checktype(jsec, JANET_NUMBER) ? janet_unwrap_integer(jsec) : 0;
- tm->tm_min = janet_checktype(jmin, JANET_NUMBER) ? janet_unwrap_integer(jmin) : 0;
- tm->tm_hour = janet_checktype(jhour, JANET_NUMBER) ? janet_unwrap_integer(jhour) : 0;
- tm->tm_mday = janet_checktype(jmday, JANET_NUMBER) ? janet_unwrap_integer(jmday) : 0;
- tm->tm_mon = janet_checktype(jmon, JANET_NUMBER) ? janet_unwrap_integer(jmon) : 0;
- // year is defined as since 1900, so we normalize
- tm->tm_year = janet_checktype(jyear, JANET_NUMBER) ? janet_unwrap_integer(jyear) - 1900 : 0;
- tm->tm_wday = janet_checktype(jwday, JANET_NUMBER) ? janet_unwrap_integer(jwday) : 0;
- tm->tm_yday = janet_checktype(jyday, JANET_NUMBER) ? janet_unwrap_integer(jyday) : 0;
- tm->tm_isdst = janet_keyeq(jisdst, "detect") ? -1 : (janet_truthy(jisdst) ? 1 : 0);
- return janet_wrap_abstract(tm);
-}
-
-// convenience to make a time_t from a dictionary
-JANET_FN(jnewtime,
- "",
- "") {
- janet_fixarity(argc, 1);
- JanetDictView dict = janet_getdictionary(argv, 0);
- Janet args[1] = { tm_from_vals(dict) };
- return jmktime(1, args);
-}
-
-// convenience to make a struct tm from a dictionary
-JANET_FN(jnewtm,
- "",
- "") {
- janet_fixarity(argc, 1);
- JanetDictView dict = janet_getdictionary(argv, 0);
- return tm_from_vals(dict);
-}
-
-// convenience to make a dict out of a tm
-JANET_FN(jtmdict,
- "",
- "") {
- janet_fixarity(argc, 1);
- struct tm *tm = janet_getjtm(argv, 0);
- JanetTable *out = janet_table(9);
-
- janet_table_put(out, janet_ckeywordv("sec"), janet_wrap_integer(tm->tm_sec));
- janet_table_put(out, janet_ckeywordv("min"), janet_wrap_integer(tm->tm_min));
- janet_table_put(out, janet_ckeywordv("hour"), janet_wrap_integer(tm->tm_hour));
- janet_table_put(out, janet_ckeywordv("mday"), janet_wrap_integer(tm->tm_mday));
- janet_table_put(out, janet_ckeywordv("mon"), janet_wrap_integer(tm->tm_mon));
- // year is defined as since 1900, so we normalize
- janet_table_put(out, janet_ckeywordv("year"), janet_wrap_integer(tm->tm_year + 1900));
- janet_table_put(out, janet_ckeywordv("wday"), janet_wrap_integer(tm->tm_wday));
- janet_table_put(out, janet_ckeywordv("yday"), janet_wrap_integer(tm->tm_yday));
- if(tm->tm_isdst == 0) {
- janet_table_put(out, janet_ckeywordv("isdst"), janet_wrap_false());
- } else if (tm->tm_isdst > 0) {
- janet_table_put(out, janet_ckeywordv("isdst"), janet_wrap_true());
- } else {
- janet_table_put(out, janet_ckeywordv("isdst"), janet_ckeywordv("detect"));
- }
- return janet_wrap_table(out);
-}
-
-JANET_FN(jgettzoffset,
- "",
- "") {
- janet_fixarity(argc, 0);
- time_t *t = janet_smalloc(sizeof(time_t));
- time(t);
- struct tm *tm = localtime(t);
- // ISO 8601 format is -/+NNNN : 5 characters + 1 for \0
- // 8 is nice and 2^n aligned, closest over 6
- char buf[8];
- strftime(buf, 8, "%z", tm);
- // manual parsing for fun
- // the offset will be in minutes
- int offset = 0;
- offset += buf[4] - '0';
- offset += (buf[3] - '0') * 10;
- offset += (buf[2] - '0') * 60;
- offset += (buf[1] - '0') * 60 * 10;
- if (buf[0] == '-') offset *= -1;
- return janet_wrap_integer(offset);
-}
-
-JANET_FN(jgettz,
- "",
- "") {
- janet_fixarity(argc, 0);
- time_t *t = janet_smalloc(sizeof(time_t));
- time(t);
- struct tm *tm = localtime(t);
- return janet_wrap_buffer(strftime_buffer("%Z", tm, NULL));
-}
-
-static const JanetRegExt cfuns[] = {
- JANET_REG("gmtime", jgmtime),
- JANET_REG("localtime", jlocaltime),
- JANET_REG("mktime", jmktime),
- JANET_REG("mktime!", jmktime_inplace),
- JANET_REG("time", jtime),
- JANET_REG("strftime", jstrftime),
- JANET_REG("tm->dict", jtmdict),
- JANET_REG("dict->tm", jnewtm),
- JANET_REG("dict->time", jnewtime),
- JANET_REG("tz", jgettz),
- JANET_REG("tzoffset", jgettzoffset),
- JANET_REG_END,
-};
-
-JANET_MODULE_ENTRY(JanetTable *env) {
- janet_cfuns_ext(env, "date/native", cfuns);
-}
diff --git a/project.janet b/project.janet
index 25759c1..d877160 100644
--- a/project.janet
+++ b/project.janet
@@ -10,4 +10,9 @@
(declare-native
:name "date/native"
- :source ["date.c"])
+ :headers ["src/polyfill.h" "src/date.h"]
+ :source ["src/main.c"
+ "src/polyfill.c"
+ "src/time.c"
+ "src/tm.c"
+ "src/util.c"])
diff --git a/src/date.h b/src/date.h
new file mode 100644
index 0000000..de0eb2b
--- /dev/null
+++ b/src/date.h
@@ -0,0 +1,28 @@
+#pragma once
+#include <janet.h>
+#include <time.h>
+#include "polyfill.h"
+
+// util.c
+JanetBuffer *strftime_buffer(const char *format, const struct tm *tm, JanetBuffer *buffer);
+struct tm *jd_tm_from_dict(JanetDictView dict);
+JanetTable *jd_tm_to_table(struct tm *tm);
+
+// time.c
+extern const JanetRegExt jd_time_cfuns[];
+time_t *jd_gettime(Janet *argv, int32_t n);
+time_t *jd_maketime(void);
+JANET_CFUN(jd_dict_time);
+JANET_CFUN(jd_gmtime);
+JANET_CFUN(jd_localtime);
+JANET_CFUN(jd_time);
+
+// tm.c
+extern const JanetRegExt jd_tm_cfuns[];
+struct tm *jd_gettm(Janet *argv, int32_t n);
+struct tm *jd_maketm(void);
+JANET_CFUN(jd_dict_tm);
+JANET_CFUN(jd_mktime);
+JANET_CFUN(jd_mktime_inplace);
+JANET_CFUN(jd_strftime);
+JANET_CFUN(jd_tm_dict);
diff --git a/src/main.c b/src/main.c
new file mode 100644
index 0000000..3626c24
--- /dev/null
+++ b/src/main.c
@@ -0,0 +1,6 @@
+#include "date.h"
+
+JANET_MODULE_ENTRY(JanetTable *env) {
+ janet_cfuns_ext(env, "date/native", jd_time_cfuns);
+ janet_cfuns_ext(env, "date/native", jd_tm_cfuns);
+}
diff --git a/src/polyfill.c b/src/polyfill.c
new file mode 100644
index 0000000..0d99916
--- /dev/null
+++ b/src/polyfill.c
@@ -0,0 +1,19 @@
+#include "date.h"
+
+#ifdef POLYFILL_CBYTES
+const char* janet_getcbytes(const Janet *argv, int32_t n) {
+ JanetByteView view = janet_getbytes(argv, n);
+ const char *cstr = (const char *)view.bytes;
+ if (strlen(cstr) != (size_t) view.len) {
+ janet_panic("bytes contain embedded 0s");
+ }
+ return cstr;
+}
+
+const char *janet_optcbytes(const Janet *argv, int32_t argc, int32_t n, const char *dflt) {
+ if (n >= argc || janet_checktype(argv[n], JANET_NIL)) {
+ return dflt;
+ }
+ return janet_getcbytes(argv, n);
+}
+#endif
diff --git a/src/polyfill.h b/src/polyfill.h
new file mode 100644
index 0000000..5fa72ac
--- /dev/null
+++ b/src/polyfill.h
@@ -0,0 +1,22 @@
+#pragma once
+#include <janet.h>
+
+#if JANET_VERSION_MAJOR < 2 && JANET_VERSION_MINOR < 28
+#define POLYFILL_CBYTES
+const char* janet_getcbytes(const Janet *argv, int32_t n);
+const char *janet_optcbytes(const Janet *argv, int32_t argc, int32_t n, const char *dflt);
+
+#if !defined(JANET_NO_SOURCEMAPS) && !defined(JANET_NO_DOCSTRINGS)
+#undef JANET_FN
+#define JANET_FN(CNAME, USAGE, DOCSTRING) \
+ static const int32_t CNAME##_sourceline_ = __LINE__; \
+ static const char CNAME##_docstring_[] = USAGE "\n\n" DOCSTRING; \
+ Janet CNAME (int32_t argc, Janet *argv)
+#elif !defined(JANET_NO_SOURCEMAPS) && defined(JANET_NO_DOCSTRINGS)
+#undef JANET_FN
+#define JANET_FN(CNAME, USAGE, DOCSTRING) \
+ static const int32_t CNAME##_sourceline_ = __LINE__; \
+ Janet CNAME (int32_t argc, Janet *argv)
+#endif // !defined(JANET_NO_SOURCEMAPS)
+
+#endif // JANET_VERSION_MAJOR < 2 && JANET_VERSION_MINOR < 28
diff --git a/src/time.c b/src/time.c
new file mode 100644
index 0000000..64096c7
--- /dev/null
+++ b/src/time.c
@@ -0,0 +1,100 @@
+#include "date.h"
+#include "janet.h"
+
+// wrappers around time_t
+
+static JanetMethod jd_time_methods[] = {
+ {"gmtime", jd_gmtime},
+ {"localtime", jd_localtime},
+ {"todict", jd_dict_time},
+ {NULL, NULL},
+};
+
+static int jd_time_compare(void *lhs, void *rhs) {
+ time_t lhv = (*(time_t*)lhs);
+ time_t rhv = (*(time_t*)rhs);
+ return difftime(lhv, rhv);
+}
+
+static int jd_time_get(void *p, Janet key, Janet *out) {
+ (void) p;
+ if (!janet_checktype(key, JANET_KEYWORD)) {
+ return 0;
+ }
+ return janet_getmethod(janet_unwrap_keyword(key), jd_time_methods, out);
+}
+
+// time_t is always a UTC-representation
+static void jd_time_tostring(void *p, JanetBuffer *buffer) {
+ strftime_buffer("%F %T.000 UTC", localtime(p), buffer);
+}
+
+static const JanetAbstractType jd_time_t = {
+ "time",
+ NULL,
+ NULL,
+ jd_time_get,
+ NULL,
+ NULL,
+ NULL,
+ jd_time_tostring,
+ jd_time_compare,
+ JANET_ATEND_COMPARE
+};
+
+time_t *jd_gettime(Janet *argv, int32_t n) {
+ return (time_t*)janet_getabstract(argv, n, &jd_time_t);
+}
+
+time_t *jd_maketime(void) {
+ return janet_abstract(&jd_time_t, sizeof(time_t));
+}
+
+JANET_FN(jd_dict_time,
+ "(dict->time {...})",
+ "") {
+ janet_fixarity(argc, 1);
+ JanetDictView dict = janet_getdictionary(argv, 0);
+ struct tm *tm = jd_tm_from_dict(dict);
+ return janet_wrap_abstract(tm);
+}
+
+JANET_FN(jd_gmtime,
+ "(gmtime (time))",
+ "") {
+ janet_fixarity(argc, 1);
+ time_t *time = jd_gettime(argv, 0);
+ struct tm *tm = gmtime(time);
+ struct tm *out = jd_maketm();
+ *out = *tm;
+ return janet_wrap_abstract(out);
+}
+
+JANET_FN(jd_localtime,
+ "(localtime (time))",
+ "WARNING: do not use this unless it's for final display.") {
+ janet_fixarity(argc, 1);
+ time_t *time = jd_gettime(argv, 0);
+ struct tm *tm = localtime(time);
+ struct tm *out = jd_maketm();
+ *out = *tm;
+ return janet_wrap_abstract(out);
+}
+
+JANET_FN(jd_time,
+ "(time)",
+ "") {
+ (void) argv;
+ janet_fixarity(argc, 0);
+ time_t *out = jd_maketime();
+ time(out);
+ return janet_wrap_abstract(out);
+}
+
+const JanetRegExt jd_time_cfuns[] = {
+ JANET_REG("dict->time", jd_dict_time),
+ JANET_REG("gmtime", jd_gmtime),
+ JANET_REG("localtime", jd_localtime),
+ JANET_REG("time", jd_time),
+ JANET_REG_END
+};
diff --git a/src/tm.c b/src/tm.c
new file mode 100644
index 0000000..5d2abff
--- /dev/null
+++ b/src/tm.c
@@ -0,0 +1,164 @@
+#include "date.h"
+#include "janet.h"
+
+// wrappers around struct tm
+
+static JanetMethod jd_tm_methods[] = {
+ {"mktime", jd_mktime},
+ {"mktime!", jd_mktime_inplace},
+ {"normalize", jd_mktime_inplace},
+ {"strftime", jd_strftime},
+ {"todict", jd_tm_dict},
+ {NULL, NULL},
+};
+
+static int jd_tm_compare(void *lhs, void *rhs) {
+ struct tm lhp = (*(struct tm*)lhs);
+ struct tm rhp = (*(struct tm*)rhs);
+ time_t lhv = mktime(&lhp);
+ time_t rhv = mktime(&rhp);
+ return difftime(lhv, rhv);
+}
+
+static int jd_tm_get(void *p, Janet key, Janet *out) {
+ if (!janet_checktype(key, JANET_KEYWORD)) {
+ return 0;
+ }
+
+ // is it a method?
+ if(janet_getmethod(janet_unwrap_keyword(key), jd_tm_methods, out)) {
+ return 1;
+ }
+
+ // piggyback off jd_tm_to_table
+ JanetTable *tb = jd_tm_to_table(p);
+ *out = janet_table_rawget(tb, key);
+
+ return janet_checktype(*out, JANET_NIL);
+}
+
+static const char* jd_tm_keys[] = {
+ "sec", "min", "hour", "mday", "mon", "year", "wday", "yday", NULL,
+};
+static Janet jd_tm_next(void *p, Janet key) {
+ (void) p;
+ const char **ptr = jd_tm_keys;
+ while (*ptr) {
+ if (janet_keyeq(key, *ptr)) {
+ return *(++ptr) ? janet_ckeywordv(*ptr) : janet_wrap_nil();
+ }
+ ptr++;
+ }
+ return janet_ckeywordv(jd_tm_keys[0]);
+}
+
+// struct tm can represent non-UTC
+// it does not keep TZ information so we can't display it without potentially lying
+static void jd_tm_tostring(void *p, JanetBuffer *buffer) {
+ strftime_buffer("%F %T.000", p, buffer);
+}
+
+static const JanetAbstractType jd_tm_t = {
+ "tm",
+ NULL,
+ NULL,
+ jd_tm_get,
+ NULL,
+ NULL,
+ NULL,
+ jd_tm_tostring,
+ jd_tm_compare,
+ NULL,
+ jd_tm_next,
+ JANET_ATEND_NEXT
+};
+
+struct tm *jd_gettm(Janet *argv, int32_t n) {
+ return (struct tm*)janet_getabstract(argv, n, &jd_tm_t);
+}
+
+struct tm *jd_maketm(void) {
+ return janet_abstract(&jd_tm_t, sizeof(struct tm));
+}
+
+JANET_FN(jd_dict_tm,
+ "",
+ "") {
+ janet_fixarity(argc, 1);
+ JanetDictView dict = janet_getdictionary(argv, 0);
+ return janet_wrap_abstract(jd_tm_from_dict(dict));
+}
+
+JANET_FN(jd_mktime,
+ "",
+ "") {
+ janet_fixarity(argc, 1);
+ struct tm *tm = jd_gettm(argv, 0);
+ struct tm *nw = jd_maketm();
+ *nw = *tm;
+ time_t *time = jd_maketime();
+ *time = mktime(nw);
+ return janet_wrap_abstract(time);
+}
+
+JANET_FN(jd_mktime_inplace,
+ "",
+ "") {
+ janet_fixarity(argc, 1);
+ struct tm *tm = jd_gettm(argv, 0);
+ time_t *time = jd_maketime();
+ *time = mktime(tm);
+ return janet_wrap_abstract(time);
+}
+
+JANET_FN(jd_tm_dict,
+ "",
+ "") {
+ janet_fixarity(argc, 1);
+ struct tm *tm = jd_gettm(argv, 0);
+ return janet_wrap_table(jd_tm_to_table(tm));
+}
+
+// strftime
+struct strftime_format {
+ const char *keyword;
+ const char *format;
+};
+const static struct strftime_format strftime_formats[] = {
+ {NULL, NULL},
+};
+JANET_FN(jd_strftime,
+ "",
+ "") {
+ janet_fixarity(argc, 2);
+ // tm is first for pseudo-OO
+ struct tm *tm = jd_gettm(argv, 0);
+
+ // determine format
+ const char *format = NULL;
+
+ // is it a preset?
+ if (janet_checktype(argv[1], JANET_KEYWORD)) {
+ const struct strftime_format *ptr = strftime_formats;
+ while (ptr->keyword) {
+ if (janet_keyeq(argv[1], ptr->keyword)) {
+ format = ptr->format;
+ break;
+ }
+ ptr++;
+ }
+ }
+
+ // preset not found
+ if (!format) format = janet_getcbytes(argv, 1);
+ return janet_wrap_buffer(strftime_buffer(format, tm, NULL));
+}
+
+const JanetRegExt jd_tm_cfuns[] = {
+ JANET_REG("dict->tm", jd_dict_tm),
+ JANET_REG("mktime", jd_mktime),
+ JANET_REG("mktime!", jd_mktime_inplace),
+ JANET_REG("strftime", jd_strftime),
+ JANET_REG("tm->dict", jd_tm_dict),
+ JANET_REG_END
+};
diff --git a/src/util.c b/src/util.c
new file mode 100644
index 0000000..3b6fd46
--- /dev/null
+++ b/src/util.c
@@ -0,0 +1,58 @@
+#include "date.h"
+
+#define JD_STRFTIME_CHUNK 64
+JanetBuffer *strftime_buffer(const char *format, const struct tm *tm, JanetBuffer *buffer) {
+ if (!buffer) buffer = janet_buffer(0);
+ size_t offset = buffer->count;
+ size_t written = 0;
+ do {
+ janet_buffer_extra(buffer, JD_STRFTIME_CHUNK);
+ written = strftime((char*)buffer->data + offset, buffer->capacity - offset, format, tm);
+ } while (!written);
+ buffer->count = written + offset; // does not include \0, but we don't want it anyway
+ return buffer;
+}
+
+static inline void tm_set_dict(JanetDictView dict, char *key, int *v) {
+ Janet k = janet_dictionary_get(dict.kvs, dict.cap, janet_ckeywordv(key));
+ *v = janet_checktype(k, JANET_NUMBER) ? janet_unwrap_integer(k) : 0;
+}
+struct tm *jd_tm_from_dict(JanetDictView dict) {
+ struct tm *tm = jd_maketm();
+
+ tm_set_dict(dict, "sec", &tm->tm_sec);
+ tm_set_dict(dict, "min", &tm->tm_min);
+ tm_set_dict(dict, "hour", &tm->tm_hour);
+ tm_set_dict(dict, "mday", &tm->tm_mday);
+ tm_set_dict(dict, "mon", &tm->tm_mon);
+ Janet year = janet_dictionary_get(dict.kvs, dict.cap, janet_ckeywordv("year"));
+ tm->tm_year = janet_checktype(year, JANET_NUMBER) ? janet_unwrap_integer(year) + 1900 : 1900;
+ tm_set_dict(dict, "wday", &tm->tm_wday);
+ tm_set_dict(dict, "yday", &tm->tm_yday);
+ Janet isdst = janet_dictionary_get(dict.kvs, dict.cap, janet_ckeywordv("isdst"));
+ tm->tm_isdst = janet_keyeq(isdst, "detect") ? -1 : (janet_truthy(isdst) ? 1 : 0);
+
+ return tm;
+}
+
+#define PUTV(T, K, V) janet_table_put(T, janet_ckeywordv(K), V)
+#define PUT(T, K, V) PUTV(T, K, janet_wrap_integer(V))
+JanetTable *jd_tm_to_table(struct tm *tm) {
+ JanetTable *out = janet_table(9);
+
+ PUT(out, "sec", tm->tm_sec);
+ PUT(out, "min", tm->tm_min);
+ PUT(out, "hour", tm->tm_hour);
+ PUT(out, "mday", tm->tm_mday);
+ PUT(out, "mon", tm->tm_mon);
+ PUT(out, "year", tm->tm_year + 1900);
+ PUT(out, "wday", tm->tm_wday);
+ PUT(out, "yday", tm->tm_yday);
+ if (tm->tm_isdst < 0) {
+ PUTV(out, "isdst", janet_ckeywordv("detect"));
+ } else {
+ PUTV(out, "isdst", tm->tm_isdst ? janet_wrap_true() : janet_wrap_false());
+ }
+
+ return out;
+}
diff --git a/test/01-native.janet b/test/01-native.janet
deleted file mode 100644
index 0c0e2fc..0000000
--- a/test/01-native.janet
+++ /dev/null
@@ -1,50 +0,0 @@
-(import date/native)
-
-# capture current time in all formats
-(def time (native/time))
-(def gmt (:gmtime time))
-(def local (:localtime time))
-
-# no crashes yet? good
-(def gd (:todict gmt))
-(def ld (:todict local))
-
-# comparisons
-# compare with +/- 1 (catch mutability, off-by-one) and +/- 120 (ensure mktime works)
-(loop [n :in [1 120]
- :let [dec |(- $ n)
- inc |(+ $ n)
- mrg |(merge $ {:sec ($1 ($ :sec))})]]
- (assert (= time (:mktime local)))
- (assert (> time (native/dict->time (mrg ld dec))))
- (assert (< time (native/dict->time (mrg ld inc))))
- (assert (= gmt (native/dict->tm gd)))
- (assert (> gmt (native/dict->tm (mrg gd dec))))
- (assert (< gmt (native/dict->tm (mrg gd inc)))))
-
-# try all of the built-in formats
-(def non-empty? (comp not zero? length))
-(loop [obj :in [gmt local]
- fmt :in [:iso8601 :locale :email :rfc5322 "%c"]]
- (assert (non-empty? (:strftime obj fmt))
- (string/format "format produced empty string: %v" fmt)))
-# try string and describe
-(loop [obj :in [['time time] ['gmt gmt] ['local local]]
- fun :in [string describe]
- :let [[sym tim] obj]]
- (assert (non-empty? (fun tim))
- (string/format "calling function %v on %v failed" fun sym)))
-
-(var ran? false)
-(eachp [k v] gmt
- (set ran? true)
- (assert (keyword? k)))
-(assert ran? "failed to iterate over tm")
-
-# test timezone detection
-# ... except when dst is on locally, it's not worth it, do not look into this
-(when (false? (ld :isdst))
- (def ld2 (merge gd {:min (+ (gd :min) (native/tzoffset))}))
- (def lm2 (native/dict->tm ld2))
- (:normalize lm2)
- (assert (= lm2 local)))