diff options
| -rw-r--r-- | CONTRIBUTING.md | 10 | ||||
| -rw-r--r-- | README.md | 110 | ||||
| -rw-r--r-- | TECH.md | 56 |
3 files changed, 144 insertions, 32 deletions
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..3d2a05e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,10 @@ +# Guidelines for Contributing to Date +Thanks for taking the time to contribute! + +This repository is relatively feature-complete. +If you can figure out a non-dirty way to add NetBSD style `_z`-terminated +functions, that would be an accepted feature. +Besides that, no further features will be accepted. + +On the other hand, additional tests (especially if they point out errors in the +codebase), example files, and bug reports are highly welcome. diff --git a/README.md b/README.md new file mode 100644 index 0000000..1456efc --- /dev/null +++ b/README.md @@ -0,0 +1,110 @@ +# Date +Complete ISO C99 date and time support for Janet. + +It contains the following components: +* `date/native`: a low level C interface to ISO C99 facilities (plus `timegm`). +* `date`: a safer janet wrapper around `date/native`. +Feel free to use either of these. + +## Installation +`jpm install https://github.com/cosmictoast/janet-date.git` + +You can add it to your `project.janet` dependency list like so: +```janet +(declare-project + :name "my-project" + :dependencies + [{:repo "https://github.com/cosmictoast/janet-date.git" + :tag "v1.0.0"}] + # ... +``` + +## Caveats +On most platforms (excluding windows), the native time.h impementation is +replaced by the [IANA's tzcode](https://data.iana.org/time-zones/tz-link.html). +It is shipped with this repository as a submodule. +However, if your code has different behavior than native libc code, +this is likely why. + +This is often desirable, since some platforms (such as macos) have incorrect +datetime handling in the libc. + +In addition, this library ships the non-standard `timegm` function. +It is provided natively in windows and in IANA's tzcode, but not on many other +platforms. + +## Examples +Note that these examples exclusively show off the janet bindings, +for native API, please see the test suite and `src/` directory. + +```janet +# there are two types of objects, date/time and date/tm +# date/time represents a specific moment in time, free of context +# you can create one that represents the current moment using date/time +(def now (date/time)) + +# the other object type is date/tm +# date/tm represents a specific calendar time +# this includes timezone information, year, day of year, and so on +# you can create a date/tm object yourself, or from a date/time object +(def calendar-now-utc (:gmtime now)) +(def calendar-now-local (:localtime now)) +# other fields are initialized to their default values, usually zero +(def calendar-unix-epoch (date/utc {:year 1970})) +# you can also use date/local to construct a date/time object +# from a local calendar time + +# you can get a date/time object from a date/tm object +# however, since date/time does not have the concept of timezones, +# you have to specify whether the date/tm object is in UTC or localtime +(def now-from-utc (:timegm calendar-now-utc)) # timegm is for GMT (UTC) +(def now-from-local (:mktime calendar-now-local)) # mktime is for localtime + +# you can compare date/time objects +(assert (= now-from-utc now-from-local)) +(assert (= now-from-utc now)) + +# you can compare date/tm objects of the same type (local or UTC) +# however, such comparisons are not very robust + +# you can print either date/time or date/tm objects in various formats +(print (date/format now)) # default format +(print (date/format now "%c")) # equivalent +(print (date/format now :html)) # standard WG formatting +(print (date/format now :html true)) # print as UTC instead of localtime + +(print (:strftime calendar-now-utc :html)) # takes same format argument +# the :strftime message is lower level, you *must* specify a format +# (:strftime calendar-now-utc) # error: arity mismatch + +# you may mutate date/tm objects +# when you call mktime or gmtime, you are normalizing the object +# you can also normalize it in-place +(put calendar-now-utc :sec 120) # out of range +(:timegm! calendar-now-utc) +# now in range +(assert > (:timegm calendar-now-utc) now) + +# however, it is recommended to work with date/time objects +# you can mutate those using the update-time function suite +(def in-a-bit + (update-time now + # you can set values directly + :hour 30 + # or you can pass an x -> x function like to `update` + :sec inc)) +(assert (> in-a-bit now)) + +# update-time is normalized to strip unsafe keys and takes keywords +# you may also use the less safe update-time* to pass a raw dict +(def in-a-bit* (update-time* now {:hour 30 :sec inc})) +(assert (= in-a-bit in-a-bit*)) + +# finally, there's a for-internal-use update-time! +# it takes a function that will apply transformations +# to the intermediate date/tm object +# it's not recommended for end-users + +# finally, you can pass nil to the update functions, in which case +# (date/time) will be used, letting you skip a step +``` @@ -1,37 +1,29 @@ -# Time is a mess -ISO C99 is fairly restrictive, while time in the wild is *wild*. +# Reading Guide +A general discussion on time, and why there isn't enough of it to discuss it. -> 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. +ISO C99 is fairly restrictive, intending to only really work with localtime. +This assumption goes fairly deep, to the point of the macos libc (for example) +presuming that all strftime calls are against localtime. -This means that `mktime` must operate on `localtime` output. -The implication (which is accurate) implies that `gmtime(t) == gmtime(mktime(localtime(t)))`. +Thankfully, IANA tzcode is more reasonable, as it handles things correctly +and provides timegm (which means you can operate in UTC until final display). -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. +On the overall, the package wraps around all ISO C99 functions that are not +marked for future deprecation, plus timegm. +The native wrapping is optimized for working primarily with time_t, only really +using struct tm for final output and intermediate representations. +This is how it's recommended to use the library as well. -`strftime` actually can handle a `gmtime`, but various platforms are buggy. -For example, macOS `%z %Z` will correctly report UTC but will report the localtime offset. -On Linux, the same format will correctly report an offset of exactly `+0000`, but claim to be in `GMT`. -As such, `%z` and `%Z` cannot be counted on. +The primary thing missing is the ability to represent an arbitrary-timezone. +For example, representing EST while localtime is CET. +Unfortunately, ISO C99 simply does not provide a way of doing this. +IANA tzcode does have ways to do this, namely via the NetBSD-inspired +`_z`-terminated functions. +Unfortunately, making those usable without completely messing up the codebase +is non-trivial. -Ok, `%z` and `%Z` don't work correctly because `struct tm` is often extended. -When I convert to and from a dictionary, I lose that information. - -Ok I've confirmed that MacOS has bugs in the libc and have now reported them. - -I've pulled in eggert tz. This helps a lot, but some details are still unfortunate (due to the spec). -The spec specifies quite explicitly that mktime operates on localtime output, which means I'll most -likely have to wrap all time_t manipulations in a localtime. -Let's start with the native api, and then the janet wrapper will purely speak time_t, I think? -I technically also have automatic "free" access to non-ISOC99 things, since they're simply -there to be used. However, I'd need to find a way to expose some libtz internals (as they can -redefine time_t, define timezone_t, etc) to get that working. -As such, I don't think I'll be doing that. +Anyway, if nothing else, something really fun that came out of this project +is that I got to report a libc bug to apple, so that's cool. +It's been almost a month and they don't seem to have acknowledged it, so if you +have to work with time, I would recommend just using IANA tzcode. +It's literally standard. |
