summaryrefslogtreecommitdiff
path: root/util.scm
diff options
context:
space:
mode:
Diffstat (limited to 'util.scm')
-rw-r--r--util.scm153
1 files changed, 153 insertions, 0 deletions
diff --git a/util.scm b/util.scm
new file mode 100644
index 0000000..495bcfd
--- /dev/null
+++ b/util.scm
@@ -0,0 +1,153 @@
+;;; SPDX-FileCopyrightText: 2026 Matthew Fennell <matthew@fennell.dev>
+;;;
+;;; SPDX-License-Identifier: AGPL-3.0-or-later
+
+;;; General
+
+(define (cartesian-product . lists)
+ (fold-right
+ (lambda (xs ys)
+ (append-map
+ (lambda (x) (map (lambda (y) (cons x y)) ys)) xs)) '(()) lists))
+
+(define* (api-request #:key method path (query #f) (request #f) (transformer identity) (token #f))
+ (let*-values
+ (((record-name)
+ (delay
+ (string-filter
+ (lambda (c) (or (char-alphabetic? c) (eq? c #\-)))
+ (symbol->string (record-type-name (record-type-descriptor request))))))
+ ((json)
+ (delay
+ (local-eval
+ `(,(string->symbol
+ (string-append (force record-name) "->json")) request)
+ (the-environment))))
+ ((base-headers) '((content-type . (application/json))
+ (origin . "https://myaccount.better.org.uk")))
+ ((auth-header) (delay `(Authorization . ,(string-append "Bearer " token))))
+ ((request-headers) (if token (cons (force auth-header) base-headers) base-headers))
+ ((request-body) (if request (force json) #f))
+ ((host) "better-admin.org.uk")
+ ((response-header response-body-utf8)
+ (http-request
+ (build-uri
+ 'https
+ #:host host
+ #:path path
+ #:query query)
+ #:method method
+ #:body request-body
+ #:headers request-headers))
+ ((response-body) (utf8->string response-body-utf8))
+ ((response-error-message) (delay (error-response-message (json->error-response response-body))))
+ ((exception)
+ (make-exception (make-external-error)
+ (make-exception-with-origin request-body)
+ (make-exception-with-irritants
+ `(,response-header ,response-body)))))
+ (log-msg 'REQUEST method " " "https://" host path "/" (or query ""))
+ (log-msg 'DEBUG "Headers: " response-header)
+ (log-msg 'SENSITIVE "Response: " response-body)
+ (case (response-code response-header)
+ ((200) (let ((result (transformer response-body)))
+ (log-msg 'SENSITIVE "Result: " result)
+ result))
+ ((422) (cond ((string-contains (force response-error-message) "The date should be within the valid days") #f)
+ ((string-contains (force response-error-message) "You have already booked a session for this time") #t)
+ (else (raise-exception exception))))
+ (else (raise-exception exception)))))
+
+(define fake-score
+ (let* ((fake-date (make-date 0 0 0 0 0 0 0 0))
+ (fake-booking (make-booking 'fake-venue 'fake-activity fake-date fake-date)))
+ `(,fake-booking ,(-(inf)))))
+
+;;; Dates
+
+(define dates #(sun mon tue wed thu fri sat))
+
+(define (date->day-name date)
+ ((compose week-day->day-name date-week-day) date))
+
+(define (week-day->day-name index)
+ (vector-ref dates index))
+
+(define (day-name->week-day name)
+ (list-index (lambda (d) (eq? d name)) (vector->list dates)))
+
+(define (date-date date)
+ (make-date 0 0 0 0
+ (date-day date)
+ (date-month date)
+ (date-year date)
+ (date-zone-offset date)))
+
+(define (date-time date)
+ (make-date 0 0
+ (date-minute date)
+ (date-hour date)
+ 0 0 0 0))
+
+(define (this day-name)
+ (log-msg 'DEBUG "Calculating next " day-name)
+ (let* ((date-offset (if (> (date-hour (current-date)) 22) 1 0))
+ (current-date (date+ (current-date) date-offset))
+ (current-week-day (date-week-day current-date))
+ (target-week-day (day-name->week-day day-name))
+ (days-ahead (mod (- target-week-day current-week-day) 7))
+ (target-date (date+ current-date days-ahead)))
+ (log-msg 'DEBUG "Including 10pm offset, current date is " (date->api-string current-date) ", which is " (week-day->day-name current-week-day))
+ (log-msg 'DEBUG "Target date " day-name " is " days-ahead " days ahead of " (week-day->day-name current-week-day))
+ (log-msg 'DEBUG "Therefore " (date->api-string current-date) " + " days-ahead " days = " ((compose date->api-string date-date) target-date))
+ (date-date target-date)))
+
+(define (date->api-string date)
+ (date->string date "~1"))
+
+(define (time->api-string date)
+ (date->string date "~H:~M"))
+
+(define (api-string->date date-str)
+ (string->date date-str "~Y-~m-~d"))
+
+(define (api-string->time time-str)
+ (string->date (string-append "1970-01-01 " time-str) "~Y-~m-~d ~H:~M"))
+
+(define (date<? date1 date2)
+ (time<? (date->time-utc date1) (date->time-utc date2)))
+
+(define (date<=? date1 date2)
+ (time<=? (date->time-utc date1) (date->time-utc date2)))
+
+(define (date>=? date1 date2)
+ (time>=? (date->time-utc date1) (date->time-utc date2)))
+
+(define (date+ date days)
+ (let ((duration-to-add (make-time time-duration 0 (* days 24 60 60)))
+ (date-time (date->time-utc (date-date date))))
+ ((compose date-date time-utc->date) (add-duration date-time duration-to-add))))
+
+(define (score>? score1 score2)
+ (> (second score1) (second score2)))
+
+(define (max-score score1 score2)
+ (if (score>? score1 score2) score1 score2))
+
+;;; Scorer helpers
+
+(define (between-hours? booking between-start-str between-end-str)
+ (let ((booking-start ((compose date-time booking-start) booking))
+ (booking-end ((compose date-time booking-end) booking))
+ (between-start ((compose date-time api-string->time) between-start-str))
+ (between-end ((compose date-time api-string->time) between-end-str)))
+ (and (date>=? booking-start between-start) (date<=? booking-end between-end))))
+
+(define (booking-end<? booking1 booking2)
+ (date<? (booking-end booking1) (booking-end booking2)))
+
+(define (badminton-60min? booking)
+ (eq? (booking-activity booking) 'badminton-60min))
+
+(define (sat? booking)
+ (eq? ((compose date->day-name booking-start) booking) 'sat))