;; ;; month.scm ;; ;; Month processing support. ;; ;; ISC License ;; ;; Copyright 2023 Brmlab, z.s. ;; Dominik Pantůček ;; ;; Permission to use, copy, modify, and/or distribute this software ;; for any purpose with or without fee is hereby granted, provided ;; that the above copyright notice and this permission notice appear ;; in all copies. ;; ;; THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL ;; WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED ;; WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE ;; AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR ;; CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS ;; OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, ;; NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN ;; CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ;; (declare (unit month)) (module month ( make-month month-year month-month month-valid? string->month month->string month=? month=? month>? month-diff month-add month-tests! ) (import scheme (chicken base) (chicken string) (chicken format) testing) ;; Simple wrapper for creating month representation as a list. (define (make-month y m) (list y m)) ;; Accessors (define month-year car) (define month-month cadr) ;; Returns true if this is a valid month representation - a list with ;; two integer elements within the allowed range. (define (month-valid? m) (and (list? m) (car m) (cdr m) (cadr m) (null? (cddr m)) (integer? (car m)) (integer? (cadr m)) (>= (car m) 1000) (<= (car m) 9999) (>= (cadr m) 1) (<= (cadr m) 12))) ;; Converts string in a format YYYY-MM to valid month. Returns #f if ;; the conversion fails. (define (string->month s) (let ((l (string-split s "-"))) (if (or (not l) (null? l) (null? (cdr l)) (not (null? (cddr l)))) #f (let ((y (string->number (car l))) (m (string->number (cadr l)))) (if (and y m) (let ((M (list y m))) (if (month-valid? M) M #f)) #f))))) ;; Formats (valid) month as YYYY-MM string (define (month->string M) (if M (if (month-valid? M) (let ((y (car M)) (m (cadr M))) (sprintf "~A-~A~A" y (if (< m 10) "0" "") m)) (error 'string->month "Invalid month" M)) "____-__")) ;; Returns true if both arguments are a valid month and are equal (define (month=? m n) (and (month-valid? m) (month-valid? n) (equal? m n))) ;; Returns true if the first argument is a month in the past of the ;; second argument month (define (month=? m n) (not (month? m n) (not (month<=? m n))) ;; Returns the number of months between from f and to t. The first ;; month is included in the count, the last month is not. (define (month-diff f t) (if (month-valid? f) (if (month-valid? t) (let ((F (+ (* (car f) 12) (cadr f) -1)) (T (+ (* (car t) 12) (cadr t) -1))) (- T F)) (error 'month-diff "Second argument is not a valid month" t)) (error 'month-diff "First argument is not a valid month" f))) ;; Returns a month n months after the month m. The number n defaults ;; to 1. (define (month-add m . ns) (let* ((n (if (null? ns) 1 (car ns))) (mi (+ (* 12 (car m)) (cadr m) n -1))) (list (quotient mi 12) (+ (remainder mi 12) 1)))) ;; Performs self-tests of the month module. (define (month-tests!) (run-tests month (test-true month-valid? (month-valid? '(2023 5))) (test-false month-valid? (month-valid? '(999 8))) (test-false month-valid? (month-valid? '(2023 -5))) (test-equal? string->month (string->month "2023-01") '(2023 1)) (test-false string->month (string->month "2023-13")) (test-false string->month (string->month "YYYY-01")) (test-false string->month (string->month "2023-MMM")) (test-equal? month->string (month->string '(2023 1)) "2023-01") (test-exn month->string (month->string '(999 12))) (test-exn month->string (month->string '(2023 13))) (test-true month