Monday, October 5, 2009

Introducing BZLIB/DATE & BZLIB/DATE-TZ - Date Time Manipulation Libraries

BZLIB/DATE & BZLIB/DATE-TZ are now available on planet. They provide additional date manipulation capability on top of SRFI/19, including timezone manipulation.

They are released under LGPL.

Usage and Installation

(require (planet bzlib/date)) 
(require (planet bzlib/date-tz)) 
bzlib/date provides date manipulations. bzlib/date provides timezone manipulations. Their usages are separately discussed below.

bzlib/date

To create a date object, you can use bulid-date, which provides a more natural year/month/day/hour/minute/second order of the parameters:

(build-date <year> <month> <day> <hour> <minute> <second> #:tz <offset>) 
And it would do the right thing if you enter February 31st:

(build-date 2009 2 31) ;; => #(struct:tm:date 0 0 0 0 3 3 2009 0) 
By default, the tz offset is 0, which equates to GMT (see below for timezone support). Only year, month, and day are required.

Date Comparisons
The following function compares two dates to determine their orders:
  • (date>? <d1> <g2>)
  • (date<? <d1> <g2>)
  • (date>=? <d1> <g2>)
  • (date<=? <d1> <g2>)
  • (day=? <d1> <g2>)
  • (date!=? <d1> <g2>)
  • (date===? <d1> <g2>)
day=? only compares the year/month/day values, and date===? means they are the same date with the same tz offset.

Conversions

You can convert between date and seconds with (date->seconds <date>) and (seconds->date <seconds>).

You can add to a date with (date+ <date> <number-of-days>). The number of day can be a non-integer.

You can find out the gaps between two dates with (date- <date1> <date2>).

You can create an alarm event with date via (date->alarm <date>) or (date->future-alarm <date>). The difference between the two is that date->future-alarm will return false if the date is in the past.

Dealing with Weekdays

To find out the weekday of a particular date, you can use (week-day <date>).

To find out the date of the nth-weekday (e.g., first sunday, 3rd wednesday, last Friday) of a particular month, use nth-week-day:

(nth-week-day <year> <month> <week-day> <nth> <hour> <minute> <second> #:tz <offset>) 
For the week-day argument, use 0 for Sunday, and 6 for Saturday. For the nth argument, use 1, 2, 3, 4, 5, or 'last. hour, minute, second, and offset are optional (same as build-date, and the other functions below that have them).

To find out the date of a particular weekday relative to another date, use one of the following:
  • week-day>=mday
  • week-day<=mday
  • week-day>mday
  • week-day<mday
They all share the same arguments, which are year, month, week-day, month-day, hour, minute, second, and offset.
The usage is something like:

;; the sunday after May 15th, 2009
(week-day>mday 2009 5 0 15) ;; 5/17/2009 
;; the friday before September 22nd, 2008 
(week-day<mday 2009 9 5 22) ;; 9/19/2009 
The hour, minute, second, and offset parameters are there for you to customize the return values:

;; the sunday after May 15th, 2009
(week-day>mday 2009 5 0 15 15 0 0 #:tz -28800) ;; 5/17/2009 15:00:00-28800
;; the friday before September 22nd, 2008 
(week-day<mday 2009 9 5 22 8 30 25 #:tz 14400) ;; 9/19/2009 08:00:00+14400

bzlib/date-tz

By default, you need to parameterize the current-tz parameter, which defaults to America/Los_Angeles. The timezone names are the available names from the olson database.

To determine the offset of any date for a particular timezone, use tz-offset:

(parameterize ((current-tz "America/New_York")) 
  (tz-offset (build-date 2008 3 9))) ;; => -18800 
(parameterize ((current-tz "America/New_York")) 
  (tz-offset (build-date 2008 3 10))) ;; => -14400 
If you want to separate between the standard offset and the daylight saving offset, you can use tz-standard-offset or tz-daylight-saving-offset:

(let ((d1 (build-date 2008 3 9))
        (d2 (build-date 2008 3 10)))
    (parameterize ((current-tz "America/New_York"))
      (values (tz-standard-offset d1)
              (tz-daylight-saving-offset d1)
              (tz-daylight-saving-offset d2))))
;; => -18800 (std)
;; => 0 (dst on 3/9/2008)
;; => 3600 (dst on 3/10/2008) 

Conversion

To reset a date's tz offset, you can use the helper function date->tz, which will reset the offset for you:

(let ((date (build-date 2008 3 10 #:tz -18800))) 
  (parameterize ((current-tz "America/New_York")) 
    (date->tz date))) 
;; => #(struct:tm:date 0 0 0 0 10 3 2008 -14400)
This function is meant for you to fix the offsets for dates that belong to a particular timezone but did not correctly account for the offset - it does not switch the timezone for you.

Couple other functions makes it even easier to work with timezone.

(build-date/tz <year> <month> ...)
(date+/tz <date> <number-of-days>)
They basically creates the date object and calls date->tz so the offset is properly adjusted based on the timezone.

Besides using current-tz, you can also pass it explicitly to tz-offset, tz-standard-offset, tz-daylight-saving-offset, date->tz, build-date/tz, and date+/tz. You pass it in in the following forms:

(tz-offset <date> "America/Los_Angeles")
(tz-daylight-saving-offset <date> "Asia/Kolkata")
(tz-standard-offset <date> "Europe/London")
(date->tz <date> "Europe/London")
(build-date/tz <year> <month> <day> #:tz "America/New_York")
(date+/tz <date> <number-of-days> "America/Los_Angeles")

Convert from One Timezone to Another

To covert the timezone of a date so you get the same date in a different timezone, use tz-convert:

(tz-convert <date> >from-timezone> <to-timezone>)
All parameters are required.

(tz-convert (build-date 2008 3 10 15) "America/New_York" "America/Los_Angeles")
;; => #(struct:tm:date 0 0 0 12 10 3 2008 -25200) ;; 2008/3/10 12:00:00-25200
(tz-convert (build-date 2008 3 10 15) "America/New_York" "GMT")
;; ==> #(struct:tm:date 0 0 0 19 10 3 2008 0) ;; 2008/10/10 19:00:00+00:00

That's it for now - enjoy.

8 comments:

  1. Hi YC,
    first of all, many thanks for the timezone library - this is very very useful!
    Finally, I'm finding time to try it out :-)

    There is one thing I don't understand - I guess it's because I have no experience with parameterize and do not exactly know how it works.

    Reading your examples, I thought the following should work:

    (parameterize ((current-tz "Europe/Berlin")) (build-date 2009 3 3))

    but it doesn't (I see I have to use build-date/tz here instead, but I'd like to understand;-) ).

    When I have

    (parameterize ((current-tz "Europe/Berlin")) (values (build-date 2009 3 3) (tz-offset (build-date 2009 3 3))))

    I get the expected offset in the first value, but the wrong one in the second. I guess it's a consequence of how parameterize works - but I'm curious...

    Ciao,
    Sigrid

    ReplyDelete
  2. Hi YC,

    it's me again. I'm experimenting further, adapting my test cases, and I see I have to call parameterize again for every new function call (not only from the tests). Would this mean I should call it once, at the start of the application?

    Thanks in advance
    Sigrid

    ReplyDelete
  3. Sigrid,

    Yes you should call parameterize at the start of the application.

    build-date and build-date/tz are two separate functions. The first version is time-zone agnostic (it's used as a base version for build-date/tz) and hence you cannot use it to determine the offset. bulid-date/tz is the version you want to use.

    Hope this helps.

    ReplyDelete
  4. Hi YC,

    thanks, I see!

    Ciao,
    Sigrid

    ReplyDelete
  5. Hi YC,

    (in the following, I had to write GE for greater/equal and LE for lower/equal to pass the validation check)

    I'd have a suggestion for the date library. Could you extend your interface such that analogous to the date=? / day=? pair, you have dayGE? and dayLE? complementing the existing dateGE=? and dateLE? ?

    In fact before using your library I had an identical day=? method to yours, and I would have needed dayGE?, but I was too lazy to implement the different cases and looked for alternatives I could use in special situations. For example, in one case I could use a quicker-to-implement "morethan24hG?" method instead, which works correct, but in another case I resorted to something which is not really what I want: I have a method

    (define event-matches-date?
    (lambda (event date)
    (and (timeG=? (date->time-utc date) (date->time-utc (event-start event))) (timeL=? (date->time-utc date) (date->time-utc (event-end event))))))

    Here I would prefer taking into account the days only, to avoid errors when events with unforeseen start or end dates come in, so it would be much safer to have

    (define event-matches-date?
    (lambda (event date)
    (and (dayG=? date (event-start event)) (dayL=? date (event-end event)))))

    Just a suggestion :-)

    Ciao
    Sigrid

    ReplyDelete
  6. Sigrid -

    sure I can included it for the next release, but just to make sure we are on the same page - when you say day<? you mean that you do not want to consider the time, as well as the timezone, right?

    If that's not the case - then date<? and date>? would be what you are looking for.

    Otherwise - I'll include a time-independent day<? and day>? in the next release.

    Cheers.

    ReplyDelete
  7. Hi YC,

    yes, I mean I do not want to consider the time at all (nor the timezone then either). Would be great if you had it in the next release :-)

    Thanks
    Sigrid

    ReplyDelete