#lang mzscheme
(require mzlib/etc
         mzlib/kw
         srfi/19
         "base.ss")
(define min-zone-offset
  (* 12 60 60 -1))
(define max-zone-offset 
  (* 12 60 60))
(define/kw (copy-date date #:key
                      [nanosecond  #f] 
                      [second      #f]
                      [minute      #f]
                      [hour        #f]
                      [day         #f]
                      [month       #f]
                      [year        #f]
                      [zone-offset #f])
  (make-date (or nanosecond  (date-nanosecond date))
             (or second      (date-second date))
             (or minute      (date-minute date))
             (or hour        (date-hour date))
             (or day         (date-day date))
             (or month       (date-month date))
             (or year        (date-year date))
             (or zone-offset (date-zone-offset date))))
(define (time->date time)
  (if (time-tai? time)
      (time-tai->date time)
      (time-utc->date time)))
(define (time-tai? datum)
  (and (time? datum)
       (eq? (time-type datum) time-tai)))
(define (time-utc? datum)
  (and (time? datum)
       (eq? (time-type datum) time-utc)))
(define (time-duration? datum)
  (and (time? datum)
       (eq? (time-type datum) time-duration)))
(define (date-valid? date)
  (let ([nanosecond (date-nanosecond date)]
        [second     (date-second date)]
        [minute     (date-minute date)]
        [hour       (date-hour date)]
        [day        (date-day date)]
        [month      (date-month date)]
        [year       (date-year date)]
        [tz         (date-zone-offset date)])
            (and (>= month 1)            (<= month 12)
         (>= day 1)              (<= day (days-in-month month year))
         (>= hour 0)             (< hour 24)
         (>= minute 0)           (< minute 60)
         (>= second 0)           (< second 60)
         (>= nanosecond 0)       (< nanosecond 1000000000)
         (>= tz min-zone-offset) (< tz max-zone-offset))))
(define (date-day-of-the-week date)
  (string->symbol (string-downcase (date->string date "~a"))))
(define (date-week-day? date)
  (and (memq (date-day-of-the-week date) '(mon tue wed thu fri)) #t))
(define (leap-year? year)
  (if (zero? (remainder year 4))
      (if (zero? (remainder year 100))
          (if (zero? (remainder year 400))
              #t
              #f)
          #t)
      #f))
(define days-in-month
  (opt-lambda (month [year 2001])     (case month
      [(9 4 6 11) 30]
      [(2) (if (leap-year? year) 29 28)]
      [(1 3 5 7 8 9 10 12) 31]
      [else (raise-exn exn:fail:contract
              (format "Month out of range: ~a" month))])))
(define seconds->ago-string 
  (opt-lambda (then [now (current-seconds)])
        (define (make-answer number unit)
      (if (= number 1)
          (if (equal? unit "day")
              "yesterday"
              (format "~a ~a ago" number unit))
          (format "~a ~as ago" number unit)))
        (define difference (- now then))
    (when (< difference 0)
      (raise-exn exn:fail:contract
        (format "Expected first argument to be less than second, received ~a ~a." then now)))
    (cond [(< difference 60)    (make-answer difference "second")]
          [(< difference 3600)  (make-answer (floor (/ difference 60)) "minute")]
          [(< difference 86400) (make-answer (floor (/ difference 3600)) "hour")]
          [else                 (make-answer (floor (/ difference 86400)) "day")])))
(define time->ago-string
  (case-lambda
    [(then)
     (let ([now (if (time-tai? then)
                    (current-time time-tai)
                    (current-time time-utc))])
       (seconds->ago-string (time-second then) (time-second now)))]
    [(then now)
     (if (eq? (time-type then) (time-type now))
         (seconds->ago-string (time-second then) (time-second now))
         (raise-exn exn:fail:contract
           (format "Arguments have different time types: ~a ~a" then now)))]))
(define (current-time-zone-offset)
  (date-zone-offset (time-tai->date (current-time time-tai))))
(define (current-year)
  (date-year (time-tai->date (current-time time-tai))))
(define time/c
  (or/c time-tai? time-utc?))
(define month/c 
  (flat-named-contract 
   "month/c"
   (lambda (x)
     (and (integer? x)
          (>= x 1)
          (<= x 12)))))
(define day-of-the-week/c
  (flat-named-contract
   "day-of-the-week/c"
   (lambda (x)
     (and (memq x '(mon tue wed thu fri sat sun)) #t))))
(provide copy-date)
(provide/contract
 [time->date               (-> time/c date?)]
 [time-tai?                procedure?]
 [time-utc?                procedure?]
 [time-duration?           procedure?]
 [date-valid?              (-> date? boolean?)]
 [date-day-of-the-week     (-> date? day-of-the-week/c)]
 [date-week-day?           (-> date? boolean?)]
 [leap-year?               (-> integer? boolean?)]
 [days-in-month            (->* (month/c) (integer?) integer?)]
 [seconds->ago-string      (->* (integer?) (integer?) string?)]
 [time->ago-string         (->* (time/c) (time/c) string?)]
 [current-time-zone-offset (-> integer?)]
 [current-year             (-> integer?)])