Years, Months, Days
February 11, 2014
This is trickier than it looks. Let’s take as an example the birthday of Alonzo Church, who would be 110 years, 7 months and 28 days old if he was still alive today:
(1) (2) (3)
2014-2-11 2014-1-42 2013-13-42
1903-6-14 1903-6-14 1903-06-14
---- -- --
110 7 28
In (1), we write the two dates with components aligned, year then month then day, with the oldest date on the bottom. Then we begin subtracting, working right to left. Since we can’t subtract the days column with 11 < 14, we "borrow" 31 days from the prior month, obtaining (2); note that February 11th is equivalent to January 42nd, if there was such a thing. Now we can't subtract in the months column, since 1 < 6, so we borrow 12 months from the prior year, obtaining (3). Now we subtract in each column to get the result.
To write the code, we encapsulate the date in a structure:
(define-structure date year month day)
Then we write function between
that computes the years, months and days between two dates:
(define (between lo hi)
(if (lt? hi lo) (between hi lo)
(let ((borrows (vector 31 31 28 31 30 31 30 31 31 30 31 30)))
(when (leap? (date-year hi)) (vector-set! borrows 2 29))
(when (< (date-day hi) (date-day lo))
(set-date-day! hi
(+ (date-day hi)
(vector-ref borrows (- (date-month hi) 1))))
(set-date-month! hi (- (date-month hi) 1)))
(when (< (date-month hi) (date-month lo))
(set-date-month! hi (+ (date-month hi) 12))
(set-date-year! hi (- (date-year hi) 1)))
(values (- (date-year hi) (date-year lo))
(- (date-month hi) (date-month lo))
(- (date-day hi) (date-day lo))))))
First we arrange the dates according to which is earlier. Then we set the numbers of days that can be borrowed each month — it is convenient to run the days from December to November, rather than from January to December, since Scheme arrays index from zero — and adjust February for leap years. The first when
checks that the days column is feasible, and borrows days from the months column if necessary. The second when
checks that the months column is feasible, and borrows months from the years column if necessary. Then the subtraction is performed individually in columns and the result is returned. We used auxiliary function to determine which date is earlier and to identify leap years:
(define (lt? date1 date2)
(cond ((< (date-year date1) (date-year date2)) #t)
((< (date-year date2) (date-year date1)) #f)
((< (date-month date1) (date-month date2)) #t)
((< (date-month date2) (date-month date1)) #f)
(else (< (date-day date1) (date-day date2)))))
(define (leap? year)
(if (zero? (modulo year 100))
(zero? (modulo year 400))
(zero? (modulo year 4))))
> (between (make-date 2014 2 11) (make-date 1903 6 14))
110
7
28
It’s possible for this function to fail, in the case where borrowing 28 or 29 days from February isn’t enough to satisfy the difference in days; for instance, from January 31st to March 1st is 1 month and 1 day by any reasonable calculation, even in a leap year, but this function returns 1 month and -2 days in a normal year ans 1 month and -1 days in a leap year. That’s hard to fix, and it’s inherent in the irregularity of the calendar, so we won’t bother, though it is instructive to consider how you might deal with that situation. You should at least ensure that your function says there is 1 day between February 28th and March 1st in a normal year and 2 days in a leap year.
We used structures from the Standard Prelude. You can run the program at http://programmingpraxis.codepad.org/xNVvQKjM.
any rule against using standard Date APIs?
Isn’t it more fun to roll your own?