Skip to content

basho/jam

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

44 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Overview

Jam (from an Indonesian word for "clock" or "time") is a time parsing and processing library written to allow Riak's timeseries database to use fine-grained date and time types.

Jam's initial release includes an ISO 8601 parser/formatter, but the architecture allows for any syntax to be supported by adding a new module that understands Jam's date/time structures.

There are several phases of processing available to allow an application to massage the data for more sophisticated purposes than merely translating strings to UNIX epoch time.

Phases

Parsing

Currently Jam supports (most of) ISO 8601 but other formats are anticipated.

Yet to be supported:

  • Date/time intervals
  • Week dates

See jam_iso8601:parse/{1,2}.

Compilation

The compilation step (jam:compile/{1,2}) converts the strings captured by parsing into a (possibly valid) date and/or time. The resulting tuples will not be the same as Erlang's date/time tuples because this library permits fractional seconds, "incomplete" dates and times, and time zones.

Validation

Validation is entirely optional. It is possible to extensively manipulate invalid date/time values, and developers are advised to validate at whatever point in the pipeline seems reasonable (but no earlier than compilation).

The compilation step only exercises as much knowledge about "real" times and dates as is necessary (e.g., knowing what years are leap years to properly interpret ordinal dates). A time such as "25:15" is a possible outcome of the parsing and compilation steps, so validation functions is_valid/{1,2} are supplied.

Also see Expansion below for a discussion of incompleteness. An incomplete date or time can still be valid.

Normalization

There are two edge case time values which may arise, and which are corrected for using jam:normalize/1.

The first, permitted by ISO 8601, is "24:00". This is not the same as "00:00", at least when also attached to a date. "2016-06-15 24:00" is the same as "2016-06-16 00:00" and normalization will convert the former into the latter.

The second unusual time value: seconds > 59.

Occasionally leap seconds will be added at the end of a day. UNIX/POSIX time will silently "absorb" that into the first second of the following day.

Thus, a second value of 60 is considered valid by the validation functions and is converted to 00 seconds in the following minute during normalization.

Expansion

ISO 8601 allows for incomplete date/time strings.

We want a complete date/time for some conversions, so expansion of an incomplete date/time record via jam:expand/2 is an optional step in the processing timeline.

The values added to expand an incomplete record are the lower bounds for the appropriate fields, so 1 for month or day, 0 for any time field.

jam:is_complete/1 can be used to test a datetime structure (compiled or not) for completeness. Use jam:is_complete_date/1 or jam:is_complete_time/1 for dates and times outside a datetime structure.

It may be useful to adjust a compiled, incomplete date/time via jam:increment/2. This identifies the least significant populated value and adds an integer value. One example is strictly greater-than comparisons in Riak's timeseries support.

Translation

Dates and times can be converted to alternative time zones (jam:convert_tz/2), to ISO 8601 (and, in the future, other formats) (jam_iso8601:to_string/{1,2}), to Erlang's date/time tuples (see jam_erlang), or to integers (UNIX epoch with support for sub-second values via jam:to_epoch/{1,2}).

jam:round_fractional_seconds/1 is recommended before converting to Erlang tuples.

Jam's internal records are not intended to be manipulated or examined by code outside the library, thus doing so places your code at risk of breaking with future releases.

UTC epoch integers can also be used to create jam datetime records: see jam:from_epoch/{1,2}.

Illustrated usage

1> jam_iso8601:parse("1985-04-23 13:15:57Z").
{parsed_datetime,{parsed_calendar,"1985","04","23"},
                 {parsed_time,"13","15","57",undefined,undefined,
                              {parsed_timezone,"Z",undefined,undefined}}}
2> jam:compile(v(-1)).
{datetime,{date,1985,4,23},
          {time,13,15,57,undefined,undefined,{timezone,"Z",0,0}}}
3> jam:normalize(v(-1)).
{datetime,{date,1985,4,23},
          {time,13,15,57,undefined,undefined,{timezone,"Z",0,0}}}
4> jam:to_epoch(v(-1)).
483110157
5> jam_iso8601:to_string(v(-2)).
"1985-04-23T13:15:57Z"
6> jam_iso8601:to_string(v(-3), [{format, basic}]).
"19850423T131557Z"
7> jam_iso8601:to_string(v(-4), [{format, basic}, {z, false}]).
"19850423T131557+0000"

For a more sophisticated example, we handle a leap second with a fractional component and time zone conversion.

Note that in order to support passing times without dates as arguments, jam:offset_normalize/1, jam:offset_round_fractional_seconds/1 and jam:offset_convert_tz/2 return a two-tuple, with the first value an integer expressing whether or not a date adjustment resulted. The non-offset_ version of each function drops the date adjustment in favor of a single return value, and we'll use those here.

1> DT1 = jam:normalize(jam:compile(jam_iso8601:parse("20150630T23:59:60.738Z"))).
{datetime,{date,2015,7,1},
          {time,0,0,0,
                {fraction,0.738,3},
                undefined,
                {timezone,"Z",0,0}}}
2> DT2 = jam:round_fractional_seconds(DT1).
{datetime,{date,2015,7,1},
          {time,0,0,1,undefined,undefined,{timezone,"Z",0,0}}}
3> DT3 = jam:convert_tz(DT2, "-07:30").
{datetime,{date,2015,6,30},
          {time,16,30,1,undefined,undefined,
                {timezone,"-07:30",7,30}}}
4> jam_iso8601:to_string(DT3).
"2015-06-30T16:30:01-07:30"

Here's an example of expanding an incomplete date. Note that if we specify a target for our expansion of less than a day, the original date record becomes a datetime so we can expand the time as well.

2> DT = jam:compile(jam_iso8601:parse("2016")).
#date{year = 2016,month = undefined,day = undefined}
3> jam:expand(DT, day).
#date{year = 2016,month = 1,day = 1}
4> jam:expand(DT, minute).
#datetime{date = #date{year = 2016,month = 1,day = 1},
          time = #time{hour = 0,minute = 0,second = undefined,
                       fraction = undefined,subsecond = undefined,
                       timezone = undefined}}

Dates or times can be managed independently. This snippet also illustrates compiling the ISO 8601 regular expressions for better parsing performance.

1> REs = jam_iso8601:init().
[{time,{re_pattern,14,0,
                   <<69,82,67,80,152,1,0,0,16,0,0,0,1,0,0,0,14,0,3,0,0,0,
                     ...>>}},
 {ordinal_date,{re_pattern,3,0,
                           <<69,82,67,80,125,0,0,0,16,0,0,0,1,0,0,0,3,0,0,0,0,...>>}},
 {calendar_date,{re_pattern,6,0,
                            <<69,82,67,80,172,0,0,0,16,0,0,0,1,0,0,0,6,0,3,0,...>>}},
 {week_date,{re_pattern,7,0,
                        <<69,82,67,80,174,0,0,0,16,0,0,0,5,0,0,0,7,0,3,...>>}},
 {ordinal_datetime,{re_pattern,17,0,
                               <<69,82,67,80,15,2,0,0,16,0,0,0,1,0,0,0,17,0,...>>}},
 {week_datetime,{re_pattern,21,0,
                            <<69,82,67,80,69,2,0,0,16,0,0,0,5,0,0,0,21,...>>}},
 {calendar_datetime,{re_pattern,20,0,
                                <<69,82,67,80,62,2,0,0,16,0,0,0,1,0,0,0,...>>}},
 {timezone,{re_pattern,5,0,
                       <<69,82,67,80,177,0,0,0,16,0,0,0,1,0,0,...>>}}]
2> Time = jam_iso8601:parse(REs, "T14:15:16-05").
{parsed_time,"14","15","16",undefined,undefined,
             {parsed_timezone,"-05","-05",undefined}}

Jam also allows Erlang datetime tuples to be converted to strings.

The iso8601_tests module further illustrates usage of the jam library.

Joe's Abstract Machine

The term "JAM" in Erlang originally referred to Joe Armstrong's prolog-based Erlang VM. The name collision is purely coincidental, but convenient in that there appear to be no existing Erlang libraries named jam (perhaps as a consequence).