diff options
| author | 2023-05-12 14:53:20 -0400 | |
|---|---|---|
| committer | 2023-05-12 14:53:20 -0400 | |
| commit | 78e775a1b89c827ad2e96b73a723c7caf83f5a82 (patch) | |
| tree | 11baea453275a2c9d1d22a64bcbdb9344be0cfb7 | |
| parent | update many things (diff) | |
rewrite
Split it into multiple things.
The new tests will actually reflect how you should use the native
library.
| -rw-r--r-- | TECH.md | 18 | ||||
| -rw-r--r-- | date.c | 418 | ||||
| -rw-r--r-- | project.janet | 7 | ||||
| -rw-r--r-- | src/date.h | 28 | ||||
| -rw-r--r-- | src/main.c | 6 | ||||
| -rw-r--r-- | src/polyfill.c | 19 | ||||
| -rw-r--r-- | src/polyfill.h | 22 | ||||
| -rw-r--r-- | src/time.c | 100 | ||||
| -rw-r--r-- | src/tm.c | 164 | ||||
| -rw-r--r-- | src/util.c | 58 | ||||
| -rw-r--r-- | test/01-native.janet | 50 |
11 files changed, 421 insertions, 469 deletions
@@ -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. @@ -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))) |
