#!/usr/bin/env sh
# -*- scheme -*-
exec guile -e '(@@ (examples newbase60) main)' -s "$0" "$@"
; !#
;; Encoding and decoding numbers in New Base 60 as defined by Tantek:
;; http://tantek.pbworks.com/NewBase60
;; Based on the very elegant implementation from Kevin Marks licensed under CC0:
;; https://github.com/indieweb/newBase60py/blob/master/newbase60.py
(define-module (examples newbase60)
#:export (integer->sxg sxg->integer date->sxg sxg->date date->sxgepochdays sxgepochdays->yeardays yeardays->sxgepochdays)
#:use-module (srfi srfi-1))
(define base60letters "0123456789ABCDEFGHJKLMNPQRSTUVWXYZ_abcdefghijkmnopqrstuvwxyz")
(define base60numbers
(append
(map cons (string->list base60letters) (iota (string-length base60letters)))
(quote
((#\l . 1 ); typo lowercase l to 1
(#\I . 1 ); typo capital I to 1
(#\O . 0 ))))); typo capital O to 0
(define (positive-integer->sxg num)
"Convert a positive integer to Tanteks new base 60."
(if (equal? 0 num)
"0"
(let loop
((s '())
(n num))
(if (equal? n 0)
(list->string s)
(loop
(cons (string-ref base60letters (remainder n 60)) s)
(quotient n 60))))))
(define (positive-sxg->integer string)
"Convert a positive new base 60 string into a positive integer."
(let loop
((n 0)
(s string))
(cond
((equal? "" s)
n)
(else
(loop
(+ (assoc-ref base60numbers (string-ref s 0))
(* n 60))
(string-drop s 1))))))
(define (integer->sxg num)
"Convert an integer to Tanteks new base 60."
(if (>= num 0)
(positive-integer->sxg num)
(string-append "-" (positive-integer->sxg (- num)))))
(define (sxg->integer str)
"Convert a new base 60 string into an integer."
(if (and (>= (string-length str) 1) (equal? #\- (string-ref str 0)))
(- (positive-sxg->integer (string-drop str 1)))
(positive-sxg->integer str)))
(define (date->sxgepochdays year month day hour minute second)
(let
((tm (gmtime 0 ))); initialize
(set-tm:year tm (- year 1900))
(set-tm:mon tm month)
(set-tm:mday tm day)
(set-tm:hour tm hour)
(set-tm:min tm minute)
(set-tm:sec tm second)
(let*
((epochseconds (car (mktime tm "+0" ))); 0: UTC
(epochdays (quotient epochseconds (* 24 60 60))))
(integer->sxg epochdays))))
(define (yeardays->sxgepochdays year yeardays)
(let
((tm (car (strptime "%Y %j" (string-join (map number->string (list year yeardays)))))))
(let*
((epochseconds (car (mktime tm "+0" ))); 0: UTC
(epochdays (quotient epochseconds (* 24 60 60))))
(integer->sxg epochdays))))
(define (sxgepochdays->yeardays str)
"Turn sexagesimal days since epoch into year (YYYY) and day of year (DDD)."
(let*
((epochdays (sxg->integer str))
(epochseconds (* epochdays 24 60 60))
(tm (gmtime epochseconds))
(year (+ 1900 (tm:year tm)))
(yeardays (tm:yday tm)))
(list year (+ yeardays 1))))
(define (date->sxg year month day hour minute second)
"Convert a date into new base 60 format:
yyyymmdd hhmmss -> YYMD-hms (can extend till 3599)
"
(format #f "~A-~A"
(apply string-append
(map integer->sxg
(list year month day)))
(apply string-append
(map integer->sxg
(list hour minute second)))))
(define (sxg->date str)
"Convert a new base 60 date into a list:
YYMD-hms -> (year month day hour minute second)
"
(let*
((centeridx (string-rindex str #\- )); rindex because the year could be negative
(getstr (lambda (s di) (string (string-ref str (+ centeridx di))))))
(let
((year (substring/read-only str 0 (- centeridx 2)))
(month (getstr str -2))
(day (getstr str -1))
(hour (getstr str 1))
(minute (getstr str 2))
(second (getstr str 3)))
(map sxg->integer
(list year month day hour minute second)))))
(define (main args)
(let
((help (lambda () (format #t "usage: ~A [integer | -d string | --datetime | --datetime year month day hour minute second | --help]\n" (list-ref args 0)))))
(cond
((or (= 1 (length args)) (member "--help" args))
(help))
((and (= 8 (length args)) (equal? "--datetime" (list-ref args 1)))
(format #t "~A\n" (apply date->sxg (map string->number (drop args 2)))))
((and (= 8 (length args)) (equal? "--sxgepochdays" (list-ref args 1)))
(format #t "~A\n" (apply date->sxgepochdays (map string->number (drop args 2)))))
((and (= 4 (length args)) (equal? "--sxgepochdays-from-yearday" (list-ref args 1)))
(format #t "~A\n" (apply yeardays->sxgepochdays (map string->number (drop args 2)))))
((and (= 2 (length args)) (equal? "--datetime" (list-ref args 1)))
(let ((tm (localtime (current-time))))
(format #t "~A\n" (apply date->sxg (list (+ 1900 (tm:year tm)) (+ 1 (tm:mon tm)) (tm:mday tm) (tm:hour tm) (tm:min tm) (tm:sec tm))))))
((and (= 3 (length args)) (equal? "--decode-datetime" (list-ref args 1)))
(format #t "~A\n" (sxg->date (list-ref args 2))))
((and (= 3 (length args)) (equal? "--decode-sxgepochdays" (list-ref args 1)))
(format #t "~A\n" (sxgepochdays->yeardays (list-ref args 2))))
((and (= 3 (length args)) (equal? "-d" (list-ref args 1)))
(format #t "~A\n" (sxg->integer (list-ref args 2))))
((= 2 (length args))
(format #t "~A\n" (integer->sxg (string->number (list-ref args 1)))))
(else
(help)))))