From 4f75d945436d8c6ddc2deb5936a22bffe94b7ccc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=B6rnquist?= Date: Sat, 4 Jul 2020 06:34:52 +0200 Subject: Merge (datetime util) into (datetime). --- module/datetime.scm | 387 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 387 insertions(+) (limited to 'module/datetime.scm') diff --git a/module/datetime.scm b/module/datetime.scm index 2c0972ea..eff3fdba 100644 --- a/module/datetime.scm +++ b/module/datetime.scm @@ -10,6 +10,13 @@ :use-module (srfi srfi-9 gnu) :use-module (util) + + :use-module (srfi srfi-41) + :use-module (srfi srfi-41 util) + :use-module (ice-9 i18n) + :use-module (ice-9 format) + :use-module (util config) + :re-export (locale-month) ) (define-many define-public @@ -726,3 +733,383 @@ (read-hash-extend #\0 date-reader) (read-hash-extend #\1 date-reader) (read-hash-extend #\2 date-reader) + + + + +(define-public (start-of-month date) + (set (day date) 1)) + +(define-public (end-of-month date) + (set (day date) (days-in-month date))) + +(define-public (start-of-year date) + (set-> date + (day 1) + (month 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 (date-stream date-increment start-day) + (stream-iterate (lambda (d) (date+ d date-increment)) + start-day)) + +(define-public (day-stream start-day) + (date-stream (date day: 1) start-day)) + +(define-public (month-stream start-day) + (date-stream (date month: 1) start-day)) + +(define-public (week-stream start-day) + (date-stream (date day: 7) start-day)) + +(define-public (time-min a b) + (if (time day-index 3) + (date+ ystart (date day: (- 7 day-index))) + (date- ystart (date day: day-index))))) + +;; (week-number #2020-01-01 mon) ; => 1 +;; (week-number #2019-12-31 mon) ; => 1 +(define*-public (week-number d optional: (wkst (week-start))) + ;; Calculating week number for starts of week was much simpler. + ;; We can both skip the special cases for Jan 1, 2 & 3. It also + ;; solved some weird bug that was here before. + + (let ((d (start-of-week d wkst))) + (cond + [(and (= 12 (month d)) + (memv (day d) '(29 30 31)) + (< (year d) (year (date+ (start-of-week d wkst) + (date day: 3))))) + 1] + + [else + (let* ((w1-start (week-1-start d wkst)) + (week day (floor/ (days-in-interval w1-start d) + 7))) + (1+ week))]))) + +(define*-public (date-starting-week week-number d optional: (wkst (week-start))) + (date+ (week-1-start d wkst) + (date day: (* (1- week-number) 7)))) + + +(define*-public (week-day-name week-day-number optional: truncate-to + key: (locale %global-locale)) + + ;; NOTE this allows days larger than 7 (sunday if counting from monday). + (let ((str (catch 'out-of-range + (lambda () (locale-day (1+ (modulo week-day-number 7)) locale)) + (lambda (oor str num) (scm-error 'out-of-range 'week-day-name + "~a == (~a % 7) + 1" + (list num week-day-number) (list week-day-number)))))) + ;; I also know about the @var{locale-day-short} method, but I need + ;; strings of length 2. + (if truncate-to + (string-take str truncate-to) + str))) + +(define*-public (datetime->string datetime optional: (fmt "~Y-~m-~dT~H:~M:~S") key: allow-unknown?) + (define date (get-date datetime)) + (define time ((@ (datetime) get-time%) datetime)) + (with-output-to-string + (lambda () + (fold (lambda (token state) + (case state + ((#\~) + (case token + ((#\~) (display "~")) + ((#\H) (format #t "~2'0d" (hour time))) + ((#\k) (format #t "~2' d" (hour time))) + ((#\M) (format #t "~2'0d" (minute time))) + ((#\S) (format #t "~2'0d" (second time))) + ((#\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 + ((#\s) (display (datetime->unix-time datetime))) ; epoch time! + ((#\e) (format #t "~2' d" (day date))) + ((#\1) (format #t "~4'0d-~2'0d-~2'0d" + (year date) (month date) (day date))) + ((#\3) (format #t "~2'0d:~2'0d:~2'0d" + (hour time) (minute time) (second time))) + ((#\A) (display (week-day-name (week-day date)))) + ((#\a) (display (week-day-name (week-day date) 3))) + ((#\b) (display (locale-month-short (month date)))) + ((#\B) (display (locale-month (month date)))) + ((#\Z) (when (equal? "UTC" (get-timezone datetime)) (display "Z"))) + (else (unless allow-unknown? + (error 'datetime->string "Invalid format token ~a" token)))) + #f) + (else (unless (char=? #\~ token) (display token)) token))) + #f + (string->list fmt))))) + +(define*-public (date->string date optional: (fmt "~Y-~m-~d") key: allow-unknown?) + (datetime->string (datetime date: date) fmt allow-unknown?: allow-unknown?)) + +(define*-public (time->string time optional: (fmt "~H:~M:~S") key: allow-unknown?) + (datetime->string (datetime time: time) fmt allow-unknown?: allow-unknown?)) + + +;; @verbatim +;; A B C D E ¬F +;; |s1| : |s2| : |s1| : |s2| : : |s1| +;; | | : | | : | ||s2| : |s1|| | : |s1||s2| : | | +;; | ||s2| : |s1|| | : | || | : | || | : | || | : +;; | | : | | : | || | : | || | : | || | : |s2| +;; | | : | | : | | : | | : : | | +;; +;; Infinitely short ---+|s2| : |s1|+--- : two instants don't overlap +;; events, overlap 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 (0 1 2 3 4 5 6) +;; @end example +(define-public (weekday-list week-start) + (take (drop (apply circular-list (iota 7)) + week-start) + 7)) + +;; returns the date the week containing d started. +;; (start-of-week #2020-04-02 sun) ; => 2020-03-29 +(define*-public (start-of-week d optional: (week-start (week-start))) + (date- d (date day: (modulo (- (week-day d) + week-start) + 7)))) + +;; (end-of-week #2020-04-01 mon) +;; => 2020-04-05 +(define*-public (end-of-week d optional: (week-start (week-start))) + (date+ (start-of-week d week-start) + (date day: 6))) + + +;; Given a month and and which day the week starts on, +;; returns three lists, which are: +;; The days leading up to the current month, but share a week +;; The days in the current month +;; The days after the current month, but which shares a week. +;; +;; mars 2020 +;; må ti on to fr lö sö +;; 1 +;; 2 3 4 5 6 7 8 +;; 9 10 11 12 13 14 15 +;; 16 17 18 19 20 21 22 +;; 23 24 25 26 27 28 29 +;; 30 31 +;; @lisp +;; (month-days #2020-03-01 mon) +;; ; ⇒ (2020-02-24 ... 2020-02-29) +;; ; ⇒ (2020-03-01 ... 2020-03-31) +;; ; ⇒ (2020-04-01 ... 2020-04-05) +;; @end lisp +;; Ignores day component of @var{date}. +(define*-public (month-days date optional: (week-start (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))))))) + +;; Day from start of the year, so 1 feb would be day 32. +;; Also known as Julian day. +(define-public (year-day date) + (days-in-interval (start-of-year date) 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)))) + +;; Returns a list of all dates from start to end. +;; both inclusive +;; date, date → [list date] +(define-public (date-range start end) + (stream->list + (stream-take-while (lambda (d) (date<= d end)) + (day-stream start)))) + + + +;; Returns the first instance of the given week-day after @var{d}. +;; @example +;; (find-first-week-day mon #2020-04-01) +;; => #2020-04-06 +;; (find-first-week-day mon #2020-04-10) +;; => #2020-04-13 +;; (find-first-week-day mon #2020-04-30) +;; => #2020-05-04 +;; @end example +(define-public (find-first-week-day wday d) + (let* ((start-day (week-day d)) + (diff (- wday start-day))) + (date+ d (date day: (modulo diff 7))))) + +;; returns instances of the given week-day in month between +;; month-date and end of month. +;; @example +;; (all-wday-in-month mon #2020-06-01) +;; => (#2020-06-01 #2020-06-08 #2020-06-15 #2020-06-22 #2020-06-29) +;; (all-wday-in-month mon #2020-06-10) +;; => (#2020-06-15 #2020-06-22 #2020-06-29) +;; @end example +;; week-day, date → (list date) +(define-public (all-wday-in-month wday month-date) + (stream->list + (stream-take-while + (lambda (d) (= (month d) (month month-date))) + (week-stream (find-first-week-day wday month-date))))) + + +(define-public (all-wday-in-year wday year-date) + (stream->list + (stream-take-while + (lambda (d) (= (year d) (year year-date))) + (week-stream (find-first-week-day wday year-date))))) -- cgit v1.2.3