(define-module (datetime util) :use-module (datetime) :use-module (srfi srfi-1) :use-module (srfi srfi-26) :use-module (srfi srfi-41) :use-module (srfi srfi-41 util) :use-module (util) ) (define-public (start-of-month date) (set (day date) 1)) (define-public (parse-freeform-date str) (let* (((year month day) (map string->number (string-split str #\-)))) (date year: year month: month day: day) )) (define-public (day-stream start-day) (stream-iterate (cut date+ <> #0-0-1) start-day)) (define-public (month-stream start-day) (stream-iterate (cut date+ <> #0-1-0) start-day)) (define-public (week-stream start-day) (stream-iterate (cut date+ <> (date day: 7)) start-day)) (define-public (time-min a b) (if (time 2020-03-16 (define-public (previous-week-start date* week-start) ((@ (srfi srfi-41 util) stream-find) (lambda (d) (= week-start (week-day d))) ((@ (srfi srfi-41) stream-iterate) (cut date- <> (date day: 1)) date*))) (define-many define-public (sun) 0 (mon) 1 (tue) 2 (wed) 3 (thu) 4 (fri) 5 (sat) 6 ) (define*-public (week-day-name week-day-number optional: truncate-to) ;; TODO internationalization (let ((str (case* week-day-number [(sun 7) "Söndag"] [(mon) "Måndag"] [(tue) "Tisdag"] [(wed) "Onsdag"] [(thu) "Torsdag"] [(fri) "Fredag"] [(sat) "Lördag"] [else (error 'argument-error "No day ~a in week" week-day-number)]))) (if truncate-to (string-take str truncate-to) str))) (define (month-name month) (case month [(1) "Jan"] [(2) "Feb"] [(3) "Mar"] [(4) "Apr"] [(5) "Maj"] [(6) "Jun"] [(7) "Jul"] [(8) "Aug"] [(9) "Sep"] [(10) "Okt"] [(11) "Nov"] [(12) "Dec"] [else (error "No month ~a" month)])) (define*-public (date->string date optional: (fmt "~Y-~m-~d") key: allow-unknown?) (with-output-to-string (lambda () (fold (lambda (token state) (case state ;; TODO add #\_ to change pad to spaces ((#\~) (case token ((#\~) (display "~")) ((#\Y) (format #t "~4'0d" (year date))) ((#\m) (format #t "~2'0d" (month date))) ((#\d) (format #t "~2'0d" (day date))) ;; Should be same as ~_d ((#\e) (format #t "~2' d" (day date))) ((#\1) (format #t "~4'0d-~2'0d-~2'0d" (year date) (month date) (day date))) ((#\a) (display (week-day-name (week-day date)))) ;; abriviated locale month name ;; TODO locale ((#\b) (display (month-name (month date)))) (else (unless allow-unknown? (error 'date->string "Invalid format token ~a" token)))) #f) (else (unless (char=? #\~ token) (display token)) token))) #f (string->list fmt))))) ;; (define*-public (time->string time optional: (fmt "~H:~M:~S") key: allow-unknown?) ;; (with-output-to-string ;; (lambda () ;; (fold (lambda (token state) ;; (case state ;; ((#\~) ;; (case token ;; ((#\~) (display "~")) ;; ((#\H) (format #t "~2'0d" (hour time))) ;; ((#\M) (format #t "~2'0d" (minute time))) ;; ((#\S) (format #t "~2'0d" (second time))) ;; ;; ((#\z) (when (utc? time) (display "Z"))) ;; (else (unless allow-unknown? ;; (error 'time->string "Invalid format token ~a" token)))) ;; #f) ;; (else (unless (char=? #\~ token) (display token)) token))) ;; #f ;; (string->list fmt))))) (define*-public (datetime->string datetime optional: (fmt "~Y-~m-~dT~H:~M:~S") key: allow-unknown?) (define dt (get-datetime datetime)) (define date (get-date dt)) (define time ((@ (datetime) get-time%) dt)) (with-output-to-string (lambda () (fold (lambda (token state) (case state ((#\~) (case token ((#\~) (display "~")) ((#\H) (format #t "~2'0d" (hour time))) ((#\M) (format #t "~2'0d" (minute time))) ((#\S) (format #t "~2'0d" (second time))) ;; TODO ;; ((#\z) (when (utc? time) (display "Z"))) ((#\Y) (format #t "~4'0d" (year date))) ((#\m) (format #t "~2'0d" (month date))) ((#\d) (format #t "~2'0d" (day date))) ;; Should be same as ~_d ((#\e) (format #t "~2' d" (day date))) ((#\1) (format #t "~4'0d-~2'0d-~2'0d" (year date) (month date) (day date))) ((#\a) (display (week-day-name (week-day date)))) ;; abriviated locale month name ;; TODO locale ((#\b) (display (month-name (month date)))) (else (unless allow-unknown? (error 'datetime->string "Invalid format token ~a" token)))) #f) (else (unless (char=? #\~ token) (display token)) token))) #f (string->list fmt))))) ;; @verbatim ;; A B C D E ¬F ;; |s1| : |s2| : |s1| : |s2| : : |s1| ;; | | : | | : | ||s2| : |s1|| | : |s1||s2| : | | ;; | ||s2| : |s1|| | : | || | : | || | : | || | : ;; | | : | | : | || | : | || | : | || | : |s2| ;; | | : | | : | | : | | : : | | ;; @end verbatim ;; ;; E is covered by both case A and B. (define-public (timespan-overlaps? s1-begin s1-end s2-begin s2-end) "Return whetever or not two timespans overlap." (or ;; A (and (date/-time (2020-02-24 ... 2020-02-29) ;; => (2020-03-01 ... 2020-03-31) ;; => (2020-04-01 ... 2020-04-05) ;; TODO Currently givining a non-start-of-month date for @var{date} is an error. (define-public (month-days date week-start) (let* ((month-len (days-in-month date)) (prev-month-len (days-in-month (month- date))) (month-start (modulo (- (week-day date) week-start) 7))) (values (map (lambda (d) (set (day (month- date)) d)) (iota month-start (1+ (- prev-month-len month-start)))) (map (lambda (d) (set (day date) d)) (iota month-len 1)) (map (lambda (d) (set (day (month+ date)) d)) (iota (modulo (- (* 7 5) month-len month-start) 7) 1))))) (define-public (days-in-interval start-date end-date) (let ((diff (date-difference (date+ end-date (date day: 1)) start-date))) (with-streams (fold + (day diff) (map days-in-month (take (+ (month diff) (* 12 (year diff))) (month-stream start-date))))))) ;; @example ;; (time->decimal-hour #10:30:00) ; => 10.5 ;; @end example (define-public (time->decimal-hour time) (exact->inexact (+ (hour time) (/ (minute time) 60) (/ (second time) 3600)))) (define*-public (datetime->decimal-hour dt optional: start-date) (let ((date-diff (cond [start-date (let* ((end-date (date+ start-date (get-date dt)))) (days-in-interval start-date end-date)) ] [(or (not (zero? (month (get-date dt)))) (not (zero? (year (get-date dt))))) (error "Multi-month intervals only supported when start-date is given" dt)] [else (day (get-date dt))]))) (+ (time->decimal-hour ((@ (datetime) get-time%) dt)) (* (1- date-diff) 24))))