Move sources to separate directory.
This commit is contained in:
parent
aa7a340d51
commit
69d0b8ee10
25 changed files with 0 additions and 0 deletions
222
src/Makefile
Normal file
222
src/Makefile
Normal file
|
@ -0,0 +1,222 @@
|
|||
#
|
||||
# Makefile
|
||||
#
|
||||
# Building the binary from sources.
|
||||
#
|
||||
# ISC License
|
||||
#
|
||||
# Copyright 2023 Brmlab, z.s.
|
||||
# Dominik Pantůček <dominik.pantucek@trustica.cz>
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
.PHONY: default
|
||||
default: imports
|
||||
|
||||
.PHONY: static
|
||||
static: bbstool
|
||||
|
||||
.PHONY: all
|
||||
all: imports static
|
||||
|
||||
CSC=csc
|
||||
|
||||
BBSTOOL-DEPS=bbstool.scm testing.import.scm listing.import.scm \
|
||||
dictionary.import.scm month.import.scm period.import.scm \
|
||||
ansi.import.scm command-line.import.scm \
|
||||
members-base.import.scm utils.import.scm primes.import.scm \
|
||||
member-record.import.scm configuration.import.scm \
|
||||
progress.import.scm table.import.scm cards.import.scm \
|
||||
member-parser.import.scm members-print.import.scm \
|
||||
member-fees.import.scm members-dir.import.scm \
|
||||
csv-simple.import.scm bank-account.import.scm \
|
||||
bank-fio.import.scm members-payments.import.scm
|
||||
|
||||
BBSTOOL-SOURCES=bbstool.scm testing.scm listing.scm dictionary.scm \
|
||||
month.scm period.scm ansi.scm command-line.scm \
|
||||
members-base.scm utils.scm primes.scm member-record.scm \
|
||||
configuration.scm progress.scm table.scm cards.scm \
|
||||
members-print.scm member-parser.scm member-fees.scm \
|
||||
members-dir.scm csv-simple.scm bank-account.scm bank-fio.scm \
|
||||
members-payments.scm
|
||||
|
||||
BBSTOOL-OBJS=bbstool.o testing.o listing.o month.o period.o ansi.o \
|
||||
dictionary.o command-line.o members-base.o utils.o primes.o \
|
||||
member-record.o configuration.o progress.o table.o cards.o \
|
||||
members-print.o member-fees.o members-dir.o csv-simple.o \
|
||||
bank-account.o bank-fio.o members-payments.o member-parser.o
|
||||
|
||||
.PHONY: imports
|
||||
imports: $(BBSTOOL-DEPS)
|
||||
|
||||
bbstool: $(BBSTOOL-OBJS)
|
||||
$(CSC) -static -o $@ $(BBSTOOL-OBJS)
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -f *.c *.link *.o *.import.scm bbstool
|
||||
|
||||
################################################################
|
||||
# Module static and shared object and import source compilation
|
||||
|
||||
%.o: %.scm
|
||||
$(CSC) -c -static $<
|
||||
|
||||
%.import.scm: %.scm
|
||||
rm -f $@
|
||||
$(CSC) -P -J $<
|
||||
|
||||
################################################################
|
||||
# Modules
|
||||
|
||||
TESTING-SOURCES=testing.scm
|
||||
|
||||
testing.o: testing.import.scm
|
||||
testing.import.scm: $(TESTING-SOURCES)
|
||||
|
||||
LISTING-SOURCES=listing.scm testing.import.scm ansi.import.scm
|
||||
|
||||
listing.o: listing.import.scm
|
||||
listing.import.scm: $(LISTING-SOURCES)
|
||||
|
||||
DICTIONARY-SOURCES=dictionary.scm testing.import.scm
|
||||
|
||||
dictionary.o: dictionary.import.scm
|
||||
dictionary.import.scm: $(DICTIONARY-SOURCES)
|
||||
|
||||
MONTH-SOURCES=month.scm testing.import.scm
|
||||
|
||||
month.o: month.import.scm
|
||||
month.import.scm: $(MONTH-SOURCES)
|
||||
|
||||
PERIOD-SOURCES=period.scm testing.import.scm month.import.scm \
|
||||
configuration.import.scm
|
||||
|
||||
period.o: period.import.scm
|
||||
period.import.scm: $(PERIOD-SOURCES)
|
||||
|
||||
ANSI-SOURCES=ansi.scm testing.import.scm utils.import.scm
|
||||
|
||||
ansi.o: ansi.import.scm
|
||||
ansi.import.scm: $(ANSI-SOURCES)
|
||||
|
||||
COMMAND-LINE-SOURCES=command-line.scm testing.import.scm
|
||||
|
||||
command-line.o: command-line.import.scm
|
||||
command-line.import.scm: $(COMMAND-LINE-SOURCES)
|
||||
|
||||
MEMBERS-BASE-SOURCES=members-base.scm testing.import.scm \
|
||||
utils.import.scm dictionary.import.scm primes.import.scm \
|
||||
member-record.import.scm ansi.import.scm period.import.scm \
|
||||
month.import.scm configuration.import.scm progress.import.scm \
|
||||
table.import.scm members-dir.import.scm
|
||||
|
||||
members-base.o: members-base.import.scm
|
||||
members-base.import.scm: $(MEMBERS-BASE-SOURCES)
|
||||
|
||||
UTILS-SOURCES=utils.scm testing.import.scm
|
||||
|
||||
utils.o: utils.import.scm
|
||||
utils.import.scm: $(UTILS-SOURCES)
|
||||
|
||||
PRIMES-SOURCES=primes.scm testing.import.scm utils.import.scm
|
||||
|
||||
primes.o: primes.import.scm
|
||||
primes.import.scm: $(PRIMES-SOURCES)
|
||||
|
||||
MEMBER-RECORD-SOURCES=member-record.scm dictionary.import.scm \
|
||||
period.import.scm testing.import.scm month.import.scm \
|
||||
configuration.import.scm primes.import.scm utils.import.scm
|
||||
|
||||
member-record.o: member-record.import.scm
|
||||
member-record.import.scm: $(MEMBER-RECORD-SOURCES)
|
||||
|
||||
CONFIGURATION-SOURCES=configuration.scm month.import.scm
|
||||
|
||||
configuration.o: configuration.import.scm
|
||||
configuration.import.scm: $(CONFIGURATION-SOURCES)
|
||||
|
||||
PROGRESS-SOURCES=progress.scm
|
||||
|
||||
progress.o: progress.import.scm
|
||||
progress.import.scm: $(PROGRESS-SOURCES)
|
||||
|
||||
TABLE-SOURCES=table.scm ansi.import.scm testing.import.scm \
|
||||
utils.import.scm
|
||||
|
||||
table.o: table.import.scm
|
||||
table.import.scm: $(TABLE-SOURCES)
|
||||
|
||||
CARDS-SOURCES=cards.scm dictionary.import.scm members-base.import.scm \
|
||||
member-record.import.scm
|
||||
|
||||
cards.o: cards.import.scm
|
||||
cards.import.scm: $(CARDS-SOURCES)
|
||||
|
||||
MEMBER-PARSER-SOURCES=member-parser.scm member-record.import.scm \
|
||||
testing.import.scm dictionary.import.scm month.import.scm \
|
||||
period.import.scm utils.import.scm configuration.import.scm
|
||||
|
||||
member-parser.o: member-parser.import.scm
|
||||
member-parser.import.scm: $(MEMBER-PARSER-SOURCES)
|
||||
|
||||
MEMBERS-PRINT-SOURCES=members-print.scm dictionary.import.scm \
|
||||
member-record.import.scm month.import.scm utils.import.scm \
|
||||
table.import.scm listing.import.scm ansi.import.scm \
|
||||
period.import.scm primes.import.scm members-base.import.scm \
|
||||
configuration.import.scm bank-account.import.scm \
|
||||
member-fees.import.scm
|
||||
|
||||
members-print.o: members-print.import.scm
|
||||
members-print.import.scm: $(MEMBERS-PRINT-SOURCES)
|
||||
|
||||
MEMBER-FEES-SOURCES=member-fees.scm configuration.import.scm \
|
||||
member-record.import.scm month.import.scm table.import.scm \
|
||||
members-base.import.scm
|
||||
|
||||
member-fees.o: member-fees.import.scm
|
||||
member-fees.import.scm: $(MEMBER-FEES-SOURCES)
|
||||
|
||||
MEMBERS-DIR-SOURCES=members-dir.scm testing.import.scm \
|
||||
dictionary.import.scm utils.import.scm \
|
||||
member-record.import.scm member-parser.import.scm
|
||||
|
||||
members-dir.o: members-dir.import.scm
|
||||
members-dir.import.scm: $(MEMBERS-DIR-SOURCES)
|
||||
|
||||
CSV-SIMPLE-SOURCES=csv-simple.scm testing.import.scm \
|
||||
progress.import.scm
|
||||
|
||||
csv-simple.o: csv-simple.import.scm
|
||||
csv-simple.import.scm: $(CSV-SIMPLE-SOURCES)
|
||||
|
||||
BANK-ACCOUNT-SOURCES=bank-account.scm
|
||||
|
||||
bank-account.o: bank-account.import.scm
|
||||
bank-account.import.scm: $(BANK-ACCOUNT-SOURCES)
|
||||
|
||||
BANK-FIO-SOURCES=bank-fio.scm bank-account.import.scm \
|
||||
csv-simple.import.scm progress.import.scm
|
||||
|
||||
bank-fio.o: bank-fio.import.scm
|
||||
bank-fio.import.scm: $(BANK-FIO-SOURCES)
|
||||
|
||||
MEMBERS-PAYMENTS-SOURCES=members-payments.scm bank-account.import.scm \
|
||||
dictionary.import.scm member-fees.import.scm \
|
||||
period.import.scm
|
||||
|
||||
members-payments.o: members-payments.import.scm
|
||||
members-payments.import.scm: $(MEMBERS-PAYMENTS-SOURCES)
|
241
src/ansi.scm
Normal file
241
src/ansi.scm
Normal file
|
@ -0,0 +1,241 @@
|
|||
;;
|
||||
;; ansi.scm
|
||||
;;
|
||||
;; ANSI terminal support.
|
||||
;;
|
||||
;; ISC License
|
||||
;;
|
||||
;; Copyright 2023 Brmlab, z.s.
|
||||
;; Dominik Pantůček <dominik.pantucek@trustica.cz>
|
||||
;;
|
||||
;; 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 ansi))
|
||||
|
||||
(module
|
||||
ansi
|
||||
(
|
||||
ansi
|
||||
a:error
|
||||
a:warning
|
||||
a:success
|
||||
a:neutral
|
||||
a:default
|
||||
a:muted
|
||||
a:highlight
|
||||
ansi-string-length
|
||||
ansi-paragraph-format
|
||||
ansi-string
|
||||
ansi-tests!
|
||||
)
|
||||
|
||||
(import scheme
|
||||
(chicken base)
|
||||
(chicken string)
|
||||
(chicken irregex)
|
||||
(chicken keyword)
|
||||
testing
|
||||
utils)
|
||||
|
||||
;; Only basic ANSI colors and bold attribute support.
|
||||
(define colors
|
||||
'((#:black . 30)
|
||||
(#:red . 31)
|
||||
(#:green . 32)
|
||||
(#:yellow . 33)
|
||||
(#:blue . 34)
|
||||
(#:magenta . 35)
|
||||
(#:cyan . 36)
|
||||
(#:white . 37)
|
||||
(#:grey . 37)
|
||||
(#:brightblack . 90)
|
||||
(#:darkgrey . 90)
|
||||
(#:brightred . 91)
|
||||
(#:brightgreen . 92)
|
||||
(#:brightyellow . 93)
|
||||
(#:brightblue . 94)
|
||||
(#:brightmagenta . 95)
|
||||
(#:pink . 95)
|
||||
(#:brightcyan . 96)
|
||||
(#:brightwhite . 97)
|
||||
|
||||
(#:bgblack . 40)
|
||||
(#:bgred . 41)
|
||||
(#:bggreen . 42)
|
||||
(#:bgyellow . 43)
|
||||
(#:bgblue . 44)
|
||||
(#:bgmagenta . 45)
|
||||
(#:bgcyan . 46)
|
||||
(#:bgwhite . 47)
|
||||
(#:bggrey . 47)
|
||||
(#:bgbrightblack . 100)
|
||||
(#:bgdarkgrey . 100)
|
||||
(#:bgbrightred . 101)
|
||||
(#:bgbrightgreen . 102)
|
||||
(#:bgbrightyellow . 103)
|
||||
(#:bgbrightblue . 104)
|
||||
(#:bgbrightmagenta . 105)
|
||||
(#:bgpink . 105)
|
||||
(#:bgbrightcyan . 106)
|
||||
(#:bgbrightwhite . 107)
|
||||
|
||||
(#:default . 0)
|
||||
(#:bold . 1)))
|
||||
|
||||
;; Returns ANSI sequence changing color and/or bold attribute.
|
||||
(define (ansi . args)
|
||||
(let ((argsl
|
||||
(map
|
||||
(lambda (key-color)
|
||||
(number->string (cdr key-color)))
|
||||
(filter
|
||||
identity
|
||||
(map (lambda (arg) (assq arg colors)) args)))))
|
||||
(if (null? argsl)
|
||||
""
|
||||
(string-append "\x1b["
|
||||
(string-intersperse argsl ";")
|
||||
"m"))))
|
||||
|
||||
;; Nice styles to be used everywhere for consistency
|
||||
(define a:error (ansi #:red #:bold))
|
||||
(define a:warning (ansi #:default #:yellow))
|
||||
(define a:success (ansi #:green #:bold))
|
||||
(define a:neutral (ansi #:default #:white))
|
||||
(define a:default (ansi #:default))
|
||||
(define a:muted (ansi #:black #:bold))
|
||||
(define a:highlight (ansi #:blue #:bold))
|
||||
|
||||
;; Returns visual string length in characters skipping any ANSI CSI
|
||||
;; SGR sequences.
|
||||
;;
|
||||
;; Internal states:
|
||||
;; 0 - regular string
|
||||
;; 1 - seen escape
|
||||
;; 2 - CSI started
|
||||
(define (ansi-string-length str)
|
||||
(let loop ((lst (irregex-extract (irregex "." 'u)
|
||||
(irregex-replace (irregex "\t$" 'u)
|
||||
(irregex-replace (irregex "^\t" 'u)
|
||||
str))))
|
||||
(state 0)
|
||||
(len 0))
|
||||
(if (null? lst)
|
||||
len
|
||||
(let ((ch (car lst)))
|
||||
(case state
|
||||
((0) (if (equal? ch "\x1b")
|
||||
(loop (cdr lst) 1 len)
|
||||
(loop (cdr lst) 0 (add1 len))))
|
||||
((1) (if (equal? ch "[")
|
||||
(loop (cdr lst) 2 len)
|
||||
(loop (cdr lst) 0 len)))
|
||||
((2) (if (equal? ch "m")
|
||||
(loop (cdr lst) 0 len)
|
||||
(loop (cdr lst) 2 len))))))))
|
||||
|
||||
;; Removes all ANSI CSI SGR sequences from the string.
|
||||
(define (ansi-remove str)
|
||||
(irregex-replace/all (irregex "\x1b\\[[0-9;]*[^0-9;]" 'u) str ""))
|
||||
|
||||
;; Formats string as paragraph of maximum given width while removing
|
||||
;; all ANSI CSI SGR from it. If the first character is \t, align
|
||||
;; right, if both first and last characters are \t, align center. The
|
||||
;; alignment is not done here, but the \t are added to all lines
|
||||
;; accordingly.
|
||||
(define (ansi-paragraph-format str width)
|
||||
(let* ((strl (string->list str))
|
||||
(first-char (if (null? strl) #f (car strl)))
|
||||
(last-char (if (null? strl) #f (car (reverse strl))))
|
||||
(first-tab (eq? first-char #\tab))
|
||||
(last-tab (eq? last-char #\tab)))
|
||||
(let loop ((words (string-split
|
||||
(ansi-remove str)))
|
||||
(res '("")))
|
||||
(if (null? words)
|
||||
(string-intersperse
|
||||
(reverse
|
||||
(map
|
||||
(lambda (line)
|
||||
(string-append (if first-tab "\t" "")
|
||||
line
|
||||
(if last-tab "\t" "")))
|
||||
res))
|
||||
"\n")
|
||||
(let* ((word (car words))
|
||||
(wlen (ansi-string-length word))
|
||||
(llen (ansi-string-length (car res))))
|
||||
(loop (cdr words)
|
||||
(if (> (+ llen wlen 1) width)
|
||||
(cons word res)
|
||||
(cons (string-append (car res)
|
||||
(if (eq? (string-length (car res)) 0)
|
||||
""
|
||||
" ")
|
||||
word)
|
||||
(cdr res)))))))))
|
||||
|
||||
;; Returns a concatenation of all ANSI styles specified by this
|
||||
;; module
|
||||
(define (ansi-string . args)
|
||||
(apply string-append
|
||||
(let loop ((args args)
|
||||
(kws '())
|
||||
(res '()))
|
||||
(if (null? args)
|
||||
(let ((rres (if (null? kws)
|
||||
res
|
||||
(cons (apply ansi kws) res))))
|
||||
(reverse rres))
|
||||
(let ((arg (car args)))
|
||||
(loop (cdr args)
|
||||
(if (keyword? arg)
|
||||
(cons arg kws)
|
||||
'())
|
||||
(if (keyword? arg)
|
||||
res
|
||||
(if (null? kws)
|
||||
(cons arg res)
|
||||
(cons arg (cons (apply ansi (reverse kws)) res))))))))))
|
||||
|
||||
;; Performs ANSI module self-tests.
|
||||
(define (ansi-tests!)
|
||||
(run-tests
|
||||
ansi
|
||||
(test-equal? ansi (ansi #:red) "\x1b[31m")
|
||||
(test-equal? ansi (ansi #:nonsense) "")
|
||||
(test-equal? ansi (ansi #:default) "\x1b[0m")
|
||||
(test-eq? ansi-string-length (ansi-string-length "test") 4)
|
||||
(test-eq? ansi-string-length (ansi-string-length "\x1b[1mtest") 4)
|
||||
(test-eq? ansi-string-length (ansi-string-length "\x1b[30mtest\x1b[0m") 4)
|
||||
(test-eq? ansi-string-length (ansi-string-length "\x1b[30mščřž\x1b[0m") 4)
|
||||
(test-equal? ansi-remove (ansi-remove "\x1b[1mtest") "test")
|
||||
(test-equal? ansi-remove (ansi-remove "\x1b[30mščřž\x1b[0m") "ščřž")
|
||||
(test-equal? ansi-paragraph-format
|
||||
(ansi-paragraph-format "Formats string as paragraph of maximum given width" 80)
|
||||
"Formats string as paragraph of maximum given width")
|
||||
(test-equal? ansi-paragraph-format
|
||||
(ansi-paragraph-format "Formats string as paragraph of maximum given width" 20)
|
||||
"Formats string as\nparagraph of maximum\ngiven width")
|
||||
(test-equal? ansi-string
|
||||
(ansi-string "Hello" #:bold "World")
|
||||
"Hello\x1b[1mWorld")
|
||||
(test-equal? ansi-string
|
||||
(ansi-string "Hello" #:bold #:red "World")
|
||||
"Hello\x1b[1;31mWorld")
|
||||
))
|
||||
|
||||
)
|
74
src/bank-account.scm
Normal file
74
src/bank-account.scm
Normal file
|
@ -0,0 +1,74 @@
|
|||
;;
|
||||
;; bank-account.scm
|
||||
;;
|
||||
;; Generic bank account records.
|
||||
;;
|
||||
;; ISC License
|
||||
;;
|
||||
;; Copyright 2023 Brmlab, z.s.
|
||||
;; Dominik Pantůček <dominik.pantucek@trustica.cz>
|
||||
;;
|
||||
;; 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 bank-account))
|
||||
|
||||
(module
|
||||
bank-account
|
||||
(
|
||||
make-bank-account
|
||||
bank-account-transactions
|
||||
bank-account-number
|
||||
bank-account-bank
|
||||
bank-account-insert
|
||||
|
||||
make-bank-transaction
|
||||
bank-transaction-varsym
|
||||
bank-transaction-amount
|
||||
bank-transaction-currency
|
||||
bank-transaction-date
|
||||
bank-transaction-id
|
||||
bank-transaction-message
|
||||
bank-transaction-type
|
||||
)
|
||||
|
||||
(import scheme
|
||||
(chicken base))
|
||||
|
||||
;; Bank account is represented as a list with list with the following
|
||||
;; elements: list of transactions, account number, bank code. This
|
||||
;; allows cheap transaction prepending.
|
||||
(define (make-bank-account number bank . maybe-transactions)
|
||||
(let ((transactions (if (null? maybe-transactions)
|
||||
'()
|
||||
(car maybe-transactions))))
|
||||
(list transactions number bank)))
|
||||
|
||||
;; Trivial accessors
|
||||
(define bank-account-transactions car)
|
||||
(define bank-account-number cadr)
|
||||
(define bank-account-bank caddr)
|
||||
|
||||
;; Prepends given transaction to given bank account. It expects the
|
||||
;; transactions list to be the first element of the bank account
|
||||
;; list.
|
||||
(define (bank-account-insert account transaction)
|
||||
(cons (cons transaction (car account))
|
||||
(cdr account)))
|
||||
|
||||
;; Creates a new bank transaction.
|
||||
(define-record bank-transaction id date amount currency varsym message type)
|
||||
|
||||
)
|
77
src/bank-fio.scm
Normal file
77
src/bank-fio.scm
Normal file
|
@ -0,0 +1,77 @@
|
|||
;;
|
||||
;; bank-fio.scm
|
||||
;;
|
||||
;; Fio CSV statements loader to common bank format.
|
||||
;;
|
||||
;; ISC License
|
||||
;;
|
||||
;; Copyright 2023 Brmlab, z.s.
|
||||
;; Dominik Pantůček <dominik.pantucek@trustica.cz>
|
||||
;;
|
||||
;; 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 bank-fio))
|
||||
|
||||
(module
|
||||
bank-fio
|
||||
(
|
||||
bank-fio-parse
|
||||
)
|
||||
|
||||
(import scheme
|
||||
(chicken base)
|
||||
(chicken string)
|
||||
bank-account
|
||||
csv-simple
|
||||
progress)
|
||||
|
||||
;; Conversion of Fio date to ISO
|
||||
(define (fio-date->iso str)
|
||||
(string-intersperse
|
||||
(reverse
|
||||
(string-split str "."))
|
||||
"-"))
|
||||
|
||||
;; Converts Fio account statement transaction row into standardized
|
||||
;; bank transaction structure.
|
||||
(define (make-fio-transaction row)
|
||||
(let ((id (string->number (car row)))
|
||||
(date (fio-date->iso (cadr row)))
|
||||
(amount (string->number
|
||||
(string-translate* (caddr row)
|
||||
'(("," . ".")))))
|
||||
(currency (string->symbol (cadddr row)))
|
||||
(varsym (list-ref row 9))
|
||||
(message (list-ref row 12))
|
||||
(type (list-ref row 13)))
|
||||
(make-bank-transaction id date amount currency varsym message type)))
|
||||
|
||||
;; Loads Fio bank accound statement.
|
||||
(define (bank-fio-parse fn)
|
||||
(with-progress%
|
||||
#t fn
|
||||
(let* ((csv (csv-parse fn))
|
||||
(head+body (csv-split-header csv))
|
||||
(head (car head+body))
|
||||
(body (cadr head+body))
|
||||
(numrow (assoc "accountId" head))
|
||||
(num (if numrow (cadr numrow) "ERROR"))
|
||||
(bankrow (assoc "bankId" head))
|
||||
(bank (if bankrow (cadr bankrow) "ERROR")))
|
||||
(make-bank-account num bank
|
||||
(map make-fio-transaction body)))))
|
||||
|
||||
)
|
199
src/bbstool.scm
Normal file
199
src/bbstool.scm
Normal file
|
@ -0,0 +1,199 @@
|
|||
;;
|
||||
;; bbstool.scm
|
||||
;;
|
||||
;; Brmburo system - members management tool.
|
||||
;;
|
||||
|
||||
;; The license of this file and of the whole suite.
|
||||
(define license-text "ISC License
|
||||
|
||||
Copyright 2023 Brmlab, z.s.
|
||||
Dominik Pantůček <dominik.pantucek@trustica.cz>
|
||||
|
||||
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.
|
||||
")
|
||||
|
||||
(import testing
|
||||
listing
|
||||
dictionary
|
||||
month
|
||||
period
|
||||
command-line
|
||||
utils
|
||||
ansi
|
||||
members-base
|
||||
primes
|
||||
member-record
|
||||
configuration
|
||||
table
|
||||
cards
|
||||
members-print
|
||||
member-parser
|
||||
member-fees
|
||||
members-dir
|
||||
csv-simple
|
||||
members-payments)
|
||||
|
||||
;; Print banner
|
||||
(print "bbstool 0.7 (c) 2023 Brmlab, z.s.")
|
||||
(newline)
|
||||
|
||||
;; Command-line options and configurable parameters
|
||||
(define *members-directory* (make-parameter "members"))
|
||||
(define *apikeys-file* (make-parameter "apikey.ntlm"))
|
||||
(define -member-id- (make-parameter #f))
|
||||
(define -member-nick- (make-parameter #f))
|
||||
(define -action- (make-parameter #f))
|
||||
(define -fname- (make-parameter #f))
|
||||
(define -run-tests?- (make-parameter #f))
|
||||
|
||||
;; Arguments parsing
|
||||
(command-line
|
||||
print-help
|
||||
(-h () "This help"
|
||||
(print "Command-line options:")
|
||||
(newline)
|
||||
(print-help)
|
||||
(newline)
|
||||
(exit 0))
|
||||
(-license () "Show licensing terms"
|
||||
(print license-text)
|
||||
(exit 0))
|
||||
(-members (dir) "Members base directory" (*members-directory* dir))
|
||||
(-context (n) "Member-File parser Error Context (lines)" (*member-file-context* (string->number n)))
|
||||
(-mi (id) "Specify member by id" (-member-id- (string->number id)))
|
||||
(-mn (nick) "Specify member by nick" (-member-nick- nick))
|
||||
(-info () "Print information" (-action- 'print-info))
|
||||
(-stats (file:gnuplot-data) "Get stats for all months"
|
||||
(-action- 'print-stats)
|
||||
(-fname- file:gnuplot-data))
|
||||
(-month (YYYY-MM) "Specify current month"
|
||||
(*current-month* (string->month YYYY-MM)))
|
||||
(-print () "Print given member file"
|
||||
(-action- 'print-member-file))
|
||||
(-tests () "Run self-tests upon startup" (-run-tests?- #t))
|
||||
(-idstats () "Returns information about available member ids" (-action- 'print-idstats))
|
||||
(-genid () "Generates random member id" (-action- 'genid))
|
||||
(-tstyle (style) "Use given table style: debug, ascii, unicode"
|
||||
(*table-border-style* (string->symbol style)))
|
||||
(-gencards (file:cards file:desfires) "Generates brmdoor-compatible card files"
|
||||
(-action- 'gencards)
|
||||
(-fname- (list file:cards file:desfires)))
|
||||
(-problems () "Prints all files with problems" (-action- 'problems))
|
||||
(-fees () "Prints fees table" (-action- 'fees))
|
||||
)
|
||||
|
||||
;; Run tests
|
||||
(when (-run-tests?-)
|
||||
(listing-tests!)
|
||||
(dictionary-tests!)
|
||||
(month-tests!)
|
||||
(period-tests!)
|
||||
(utils-tests!)
|
||||
(ansi-tests!)
|
||||
(command-line-tests!)
|
||||
(members-dir-tests!)
|
||||
(primes-tests!)
|
||||
(member-record-tests!)
|
||||
(member-parser-tests!)
|
||||
(table-tests!)
|
||||
(csv-simple-tests!)
|
||||
(newline))
|
||||
|
||||
;; Load the members database (required for everything anyway)
|
||||
(define MB (members-payments-process
|
||||
(load-members (*members-directory*) #t)
|
||||
(*apikeys-file*)))
|
||||
|
||||
;; If a member is specified by either id or nick, get its record
|
||||
(define mr
|
||||
(if (-member-id-)
|
||||
(let ((mr (find-member-by-id MB (-member-id-))))
|
||||
(when (not mr)
|
||||
(print "Member id " (-member-id-) " not found!"))
|
||||
mr)
|
||||
(if (-member-nick-)
|
||||
(let ((mr (find-member-by-nick MB (-member-nick-))))
|
||||
(when (not mr)
|
||||
(print "Member nick " (-member-nick-) " not found!"))
|
||||
mr)
|
||||
#f)))
|
||||
|
||||
;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
;; Perform requested action
|
||||
(case (-action-)
|
||||
((print-info)
|
||||
(newline)
|
||||
(print "Current month: " (month->string (*current-month*)))
|
||||
(newline)
|
||||
(if mr
|
||||
(let ()
|
||||
(print-member-table mr)
|
||||
(let* ((fees (member-fees-total mr))
|
||||
(credit (member-credit-total mr))
|
||||
(payments (member-payments-total mr))
|
||||
(balance (- (+ credit payments) fees)))
|
||||
(print "Total fees: " fees)
|
||||
(print "Total credit: " credit)
|
||||
(print "Total payments: " payments)
|
||||
(print "Balance: " balance)
|
||||
))
|
||||
(print-members-base-table MB))
|
||||
(newline))
|
||||
((print-stats)
|
||||
(newline)
|
||||
(parameterize ((current-output-port (open-output-file (-fname-))))
|
||||
(print-members-base-stats
|
||||
(members-base-stats MB))))
|
||||
((print-member-file)
|
||||
(cond (mr
|
||||
(newline)
|
||||
(print-member-source mr))
|
||||
(else
|
||||
(print "No member specified!"))))
|
||||
((print-idstats)
|
||||
(newline)
|
||||
(print-members-ids-stats MB)
|
||||
(newline))
|
||||
((genid)
|
||||
(newline)
|
||||
(print "New member id: " (gen-member-id MB))
|
||||
(newline))
|
||||
((gencards)
|
||||
(apply cards-export MB (-fname-)))
|
||||
((problems)
|
||||
(let ((num (let loop ((mb MB)
|
||||
(num 0))
|
||||
(if (null? mb)
|
||||
num
|
||||
(loop (cdr mb)
|
||||
(if (or (member-has-problems? (car mb))
|
||||
(member-has-highlights? (car mb)))
|
||||
(let ()
|
||||
(newline)
|
||||
(print-member-table (car mb))
|
||||
(print-member-source (car mb))
|
||||
(add1 num))
|
||||
num))))))
|
||||
(when (= num 0)
|
||||
(newline)
|
||||
(print "No problems found."))))
|
||||
((fees)
|
||||
(newline)
|
||||
(print-members-fees-table MB))
|
||||
|
||||
)
|
90
src/cards.scm
Normal file
90
src/cards.scm
Normal file
|
@ -0,0 +1,90 @@
|
|||
;;
|
||||
;; cards.scm
|
||||
;;
|
||||
;; Cards exporter as required by brmdoor.
|
||||
;;
|
||||
;; ISC License
|
||||
;;
|
||||
;; Copyright 2023 Brmlab, z.s.
|
||||
;; Dominik Pantůček <dominik.pantucek@trustica.cz>
|
||||
;;
|
||||
;; 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 cards))
|
||||
|
||||
(module
|
||||
cards
|
||||
(
|
||||
cards-export
|
||||
)
|
||||
|
||||
(import scheme
|
||||
(chicken base)
|
||||
(chicken sort)
|
||||
(chicken format)
|
||||
(chicken irregex)
|
||||
dictionary
|
||||
members-base
|
||||
member-record)
|
||||
|
||||
;; Prints single card type records.
|
||||
(define (cards-print/type mb type)
|
||||
(let* ((rmb (filter-members-by-predicate
|
||||
mb
|
||||
(lambda (mr)
|
||||
(and (member-active? mr)
|
||||
(dict-has-key? (dict-ref mr 'info) type)
|
||||
(not (null? (dict-ref (dict-ref mr 'info) type)))))))
|
||||
(recs (map (lambda (mr)
|
||||
(let ((mi (dict-ref mr 'info)))
|
||||
(cons (dict-ref mi 'nick)
|
||||
(dict-ref mi type))))
|
||||
rmb))
|
||||
(srecs (sort recs
|
||||
(lambda (a b)
|
||||
(string<? (car a)
|
||||
(car b))))))
|
||||
(let uloop ((srecs srecs))
|
||||
(when (not (null? srecs))
|
||||
(let* ((srec (car srecs))
|
||||
(nick (car srec)))
|
||||
(let cloop ((cards (cdr srec)))
|
||||
(when (not (null? cards))
|
||||
(let* ((card (car cards))
|
||||
(cardid (car card))
|
||||
(calias (cdr card))
|
||||
(aliased? (> (string-length calias) 0))
|
||||
(cardname (sprintf "~A~A~A"
|
||||
nick
|
||||
(if aliased? "." "")
|
||||
(if aliased?
|
||||
(irregex-replace/all (irregex " " 'u) calias "_")
|
||||
""))))
|
||||
(print cardname " " cardid)
|
||||
(cloop (cdr cards)))))
|
||||
(uloop (cdr srecs)))))))
|
||||
|
||||
;; Exports single card type records for all members to given file.
|
||||
(define (cards-export/type mb type fname)
|
||||
(parameterize ((current-output-port (open-output-file fname)))
|
||||
(cards-print/type mb type)))
|
||||
|
||||
;; Exports cards and desfires to the files specified.
|
||||
(define (cards-export mb cardsfn desfirefn)
|
||||
(cards-export/type mb 'card cardsfn)
|
||||
(cards-export/type mb 'desfire desfirefn))
|
||||
|
||||
)
|
152
src/command-line.scm
Normal file
152
src/command-line.scm
Normal file
|
@ -0,0 +1,152 @@
|
|||
;;
|
||||
;; command-line.scm
|
||||
;;
|
||||
;; Argument parsing on command-line with interpreter -- support.
|
||||
;;
|
||||
;; ISC License
|
||||
;;
|
||||
;; Copyright 2023 Brmlab, z.s.
|
||||
;; Dominik Pantůček <dominik.pantucek@trustica.cz>
|
||||
;;
|
||||
;; 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 command-line))
|
||||
|
||||
(module
|
||||
command-line
|
||||
(
|
||||
command-line
|
||||
command-line:parse-command-line
|
||||
command-line:print-options
|
||||
command-line-tests!
|
||||
)
|
||||
|
||||
(import scheme
|
||||
(chicken base)
|
||||
(chicken process-context)
|
||||
(chicken format)
|
||||
testing)
|
||||
|
||||
;; Consumes given number of arguments from the list and returns the
|
||||
;; remainder of the list and a list of arguments consumed.
|
||||
(define (consume-args args num)
|
||||
(let loop ((args args)
|
||||
(res '())
|
||||
(num num))
|
||||
(if (= num 0)
|
||||
(list args (reverse res))
|
||||
(if (null? args)
|
||||
(error 'consume-args "Not enough arguments" num)
|
||||
(loop (cdr args)
|
||||
(cons (car args) res)
|
||||
(- num 1))))))
|
||||
|
||||
;; Gets command-line arguments after the "--" of csi (not useful when
|
||||
;; compiled)
|
||||
(define (get-command-line-arguments . explicit-argv)
|
||||
(let* ((args (if (null? explicit-argv) (argv) explicit-argv))
|
||||
(rargs (member "--" args)))
|
||||
(if rargs
|
||||
(cdr rargs)
|
||||
(cdr args))))
|
||||
|
||||
;; Performs the actual parsing based on specification.
|
||||
(define (command-line:parse-command-line specs)
|
||||
(let loop ((args (get-command-line-arguments)))
|
||||
(when (not (null? args))
|
||||
(let* ((arg (car args))
|
||||
(specp (assoc arg specs)))
|
||||
(when (not specp)
|
||||
(error 'parse-command-line "Unknown argument" arg))
|
||||
(let* ((proc (caddr specp))
|
||||
(info (procedure-information proc))
|
||||
(nargs (- (length info) 1))
|
||||
(aargsl (consume-args (cdr args) nargs))
|
||||
(args (car aargsl))
|
||||
(aargs (cadr aargsl)))
|
||||
(apply proc aargs)
|
||||
(loop args))))))
|
||||
|
||||
;; String representation of procedure arguments.
|
||||
(define (procedure->argstring proc)
|
||||
(let* ((info (procedure-information proc))
|
||||
(args (cdr info))
|
||||
(argss (sprintf "~A" args)))
|
||||
(substring
|
||||
(substring argss 0 (- (string-length argss) 1))
|
||||
1)))
|
||||
|
||||
;; Prints options descriptions.
|
||||
(define (command-line:print-options specs)
|
||||
(let* ((descrs (map (lambda (spec)
|
||||
(list (car spec)
|
||||
(procedure->argstring (caddr spec))
|
||||
(cadr spec)))
|
||||
specs))
|
||||
(owidth (apply max (map (lambda (desc)
|
||||
(string-length (car desc)))
|
||||
descrs)))
|
||||
(awidth (apply max (map (lambda (desc)
|
||||
(string-length (cadr desc)))
|
||||
descrs))))
|
||||
(let loop ((descrs descrs))
|
||||
(when (not (null? descrs))
|
||||
(let* ((desc (car descrs))
|
||||
(opt (car desc))
|
||||
(args (cadr desc))
|
||||
(help (caddr desc)))
|
||||
(print " "
|
||||
opt
|
||||
(make-string (- owidth (string-length opt)) #\space)
|
||||
" "
|
||||
args
|
||||
(make-string (- awidth (string-length args)) #\space)
|
||||
" "
|
||||
help)
|
||||
(loop (cdr descrs)))))))
|
||||
|
||||
;; Syntax for expanding various types of options.
|
||||
(define-syntax make-option
|
||||
(syntax-rules ()
|
||||
((_ opt (args ...) help body ...)
|
||||
(list (symbol->string 'opt)
|
||||
help
|
||||
(lambda (args ...) body ...)))))
|
||||
|
||||
;; Simple syntax wrapper for command-line arguments specification and
|
||||
;; immediate parsing.
|
||||
(define-syntax command-line
|
||||
(syntax-rules ()
|
||||
((_ print-help (exps ...) ...)
|
||||
(letrec ((specs (list (make-option exps ...) ...))
|
||||
(print-help (lambda ()
|
||||
(command-line:print-options specs))))
|
||||
(command-line:parse-command-line specs)))))
|
||||
|
||||
;; Performs self-tests of the command-line module
|
||||
(define (command-line-tests!)
|
||||
(run-tests
|
||||
command-line
|
||||
(test-exn consume-args (consume-args '(1 2 3) 4))
|
||||
(test-equal? consume-args (consume-args '(1 2 3 4) 2) '((3 4) (1 2)))
|
||||
(test-equal? get-command-line-arguments (get-command-line-arguments 1 2 3) '(2 3))
|
||||
(test-equal? get-command-line-arguments (get-command-line-arguments 1 "--" 2 3) '(2 3))
|
||||
(test-equal? procedure->argstring (procedure->argstring (lambda (x . y) 1)) "x . y")
|
||||
(test-equal? procedure->argstring (procedure->argstring (lambda (x) 1)) "x")
|
||||
(test-equal? procedure->argstring (procedure->argstring (lambda () 1)) "")
|
||||
))
|
||||
|
||||
)
|
55
src/configuration.scm
Normal file
55
src/configuration.scm
Normal file
|
@ -0,0 +1,55 @@
|
|||
;;
|
||||
;; configuraiton.scm
|
||||
;;
|
||||
;; Configuration parameters used by various modules.
|
||||
;;
|
||||
;; ISC License
|
||||
;;
|
||||
;; Copyright 2023 Brmlab, z.s.
|
||||
;; Dominik Pantůček <dominik.pantucek@trustica.cz>
|
||||
;;
|
||||
;; 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 configuration))
|
||||
|
||||
(module
|
||||
configuration
|
||||
(
|
||||
*current-month*
|
||||
*member-file-context*
|
||||
*member-suspend-max-months*
|
||||
)
|
||||
|
||||
(import scheme
|
||||
(chicken base)
|
||||
(chicken time)
|
||||
(chicken time posix)
|
||||
month)
|
||||
|
||||
;; Current month - if changed, we get the actual state for given month.
|
||||
(define *current-month*
|
||||
(make-parameter
|
||||
(let ((d (seconds->local-time (current-seconds))))
|
||||
(list (+ 1900 (vector-ref d 5))
|
||||
(+ (vector-ref d 4) 1)))))
|
||||
|
||||
;; Configuration of error reporting
|
||||
(define *member-file-context* (make-parameter 3))
|
||||
|
||||
;; How long the member can be suspended without any action required?
|
||||
(define *member-suspend-max-months* (make-parameter 24))
|
||||
|
||||
)
|
144
src/csv-simple.scm
Normal file
144
src/csv-simple.scm
Normal file
|
@ -0,0 +1,144 @@
|
|||
;;
|
||||
;; csv-simple.scm
|
||||
;;
|
||||
;; Simple, incomplete and incorrect but fast CSV loader.
|
||||
;;
|
||||
;; ISC License
|
||||
;;
|
||||
;; Copyright 2023 Brmlab, z.s.
|
||||
;; Dominik Pantůček <dominik.pantucek@trustica.cz>
|
||||
;;
|
||||
;; 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 csv-simple))
|
||||
|
||||
(module
|
||||
csv-simple
|
||||
(
|
||||
csv-parse
|
||||
csv-split-header
|
||||
csv-simple-tests!
|
||||
)
|
||||
|
||||
(import scheme
|
||||
(chicken base)
|
||||
(chicken keyword)
|
||||
(chicken io)
|
||||
(chicken irregex)
|
||||
testing
|
||||
progress)
|
||||
|
||||
;; Curry version of line parser with configurable cell separator and
|
||||
;; string delimiter. Returns a list of lists of strings.
|
||||
(define ((make-csv-line-parser separator string-delimiter) line)
|
||||
(let loop ((tokens (string->list line))
|
||||
(res '())
|
||||
(state 1))
|
||||
(if (null? tokens)
|
||||
(reverse
|
||||
(map
|
||||
(lambda (cell)
|
||||
(list->string (reverse cell)))
|
||||
res))
|
||||
(let ((token (car tokens)))
|
||||
(case state
|
||||
((0) ; Parsing regular unquoted cell data - separator creates new cell
|
||||
(if (eq? token separator)
|
||||
(loop (cdr tokens)
|
||||
res
|
||||
1) ; Start a new cell
|
||||
(loop (cdr tokens)
|
||||
(cons (cons token (car res)) (cdr res))
|
||||
0)))
|
||||
((1) ; Starting a new cell - check for string delimiter
|
||||
(if (eq? token string-delimiter)
|
||||
(loop (cdr tokens)
|
||||
(cons '() res) ; If it is quoted, keep even empty strings there
|
||||
2)
|
||||
(if (eq? token separator)
|
||||
(loop (cdr tokens)
|
||||
(cons '() res) ; This was an empty cell
|
||||
1) ; Another new cell awaiting
|
||||
(loop (cdr tokens)
|
||||
(cons (list token) res) ; first token of regular new cell
|
||||
0))))
|
||||
((2) ; Parsing quoted cell data - no support for escaping string delimiter!
|
||||
(if (eq? token string-delimiter)
|
||||
(loop (cdr tokens)
|
||||
res
|
||||
0) ; There shouldn't be anything more, but it is safe to append the rest as normal unquoted data
|
||||
(loop (cdr tokens)
|
||||
(cons (cons token (car res)) (cdr res))
|
||||
2)))))))) ; Continue inside quoted data
|
||||
;; Parses given CSV lines list
|
||||
(define (csv-parse-lines lines . args)
|
||||
(let* ((separator (get-keyword #:separator args (lambda () #\;)))
|
||||
(string-delimiter (get-keyword #:string-delimiter args (lambda () #\")))
|
||||
(csv-parse-line (make-csv-line-parser separator string-delimiter))
|
||||
(total (max (sub1 (length lines)) 1)))
|
||||
(let loop ((lines lines)
|
||||
(idx 0)
|
||||
(res '()))
|
||||
(if (null? lines)
|
||||
(reverse res)
|
||||
(let ((line (car lines)))
|
||||
(progress%-advance (/ idx total))
|
||||
(loop (cdr lines)
|
||||
(add1 idx)
|
||||
(cons (csv-parse-line line)
|
||||
res)))))))
|
||||
|
||||
;; Loads given CSV file and parses its lines into lists
|
||||
(define (csv-parse fn . args)
|
||||
(let ((lines (read-lines (open-input-file fn))))
|
||||
(apply csv-parse-lines lines args)))
|
||||
|
||||
;; Splits CSV into header and body based on the first empty row.
|
||||
(define (csv-split-header csv)
|
||||
(let loop ((body csv)
|
||||
(rhead '()))
|
||||
(if (null? body)
|
||||
(list (reverse rhead) '())
|
||||
(let ((row (car body)))
|
||||
(if (null? row)
|
||||
(list (reverse rhead)
|
||||
(cdr body))
|
||||
(loop (cdr body)
|
||||
(cons row rhead)))))))
|
||||
|
||||
;; Module self-tests
|
||||
(define (csv-simple-tests!)
|
||||
(run-tests
|
||||
csv-simple
|
||||
(test-equal? csv-parse-line
|
||||
((make-csv-line-parser ";" "\"") "test;2;3")
|
||||
'("test" "2" "3"))
|
||||
(test-equal? csv-parse-line
|
||||
((make-csv-line-parser ";" "\"") "test;\"2;quoted\";3")
|
||||
'("test" "2;quoted" "3"))
|
||||
(test-equal? csv-split-header
|
||||
(csv-split-header '((1 2) () (3 4)))
|
||||
'(((1 2)) ((3 4))))
|
||||
(test-equal? csv-split-header
|
||||
(csv-split-header '((1 2) (5 6) (3 4)))
|
||||
'(((1 2) (5 6) (3 4)) ()))
|
||||
(test-equal? csv-parse-lines
|
||||
(csv-parse-lines '("a;b;c" "1;2"))
|
||||
'(("a" "b" "c") ("1" "2")))
|
||||
))
|
||||
|
||||
)
|
||||
|
180
src/dictionary.scm
Normal file
180
src/dictionary.scm
Normal file
|
@ -0,0 +1,180 @@
|
|||
;;
|
||||
;; dictionary.scm
|
||||
;;
|
||||
;; Simple dictionary implementation using assq lists.
|
||||
;;
|
||||
;; ISC License
|
||||
;;
|
||||
;; Copyright 2023 Brmlab, z.s.
|
||||
;; Dominik Pantůček <dominik.pantucek@trustica.cz>
|
||||
;;
|
||||
;; 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 dictionary))
|
||||
|
||||
(module
|
||||
dictionary
|
||||
(
|
||||
make-dict
|
||||
dict-has-key?
|
||||
dict-ref
|
||||
dict-remove
|
||||
dict-set
|
||||
dict-keys
|
||||
dict-map
|
||||
dict-filter
|
||||
dict-reduce
|
||||
dictionary-tests!
|
||||
)
|
||||
|
||||
(import scheme
|
||||
(chicken base)
|
||||
testing)
|
||||
|
||||
;; Returns an empty dictionary represented as empty list or a list of
|
||||
;; pre-initialized cons pairs.
|
||||
(define (make-dict . pairs)
|
||||
(if (null? pairs)
|
||||
'()
|
||||
(car pairs)))
|
||||
|
||||
;; Checks whether given dictionary d contains the key k.
|
||||
(define (dict-has-key? d k)
|
||||
(if (assq k d) #t #f))
|
||||
|
||||
;; Retrieves the value for key k from dictionary d. If third argument
|
||||
;; is provided it is used as default value in case the key does not
|
||||
;; exist. If only two arguments are given and the key does not exist,
|
||||
;; raises an error.
|
||||
(define (dict-ref d k . r)
|
||||
(let ((p (assq k d)))
|
||||
(if p
|
||||
(cdr p)
|
||||
(if (null? r)
|
||||
(error 'dict-ref "Key does not exist" k)
|
||||
(car r)))))
|
||||
|
||||
;; Returns a new dictionary based on d with key k removed. If it
|
||||
;; doesn't contain the key, an error is raised.
|
||||
(define (dict-remove d k)
|
||||
(let loop ((s d)
|
||||
(r '())
|
||||
(e #t))
|
||||
(if (null? s)
|
||||
(if e
|
||||
(error 'dict-remove "Key does not exist" k)
|
||||
r)
|
||||
(if (eq? (caar s) k)
|
||||
(loop (cdr s) r #f)
|
||||
(loop (cdr s) (cons (car s) r) e)))))
|
||||
|
||||
;; Adds a new value v under the key k to the dictionary d possibly
|
||||
;; overwriting any value which has been stored under the key
|
||||
;; before. Returns the updated dictionary.
|
||||
(define (dict-set d k v)
|
||||
(let ((dr (let loop ((s d)
|
||||
(r '()))
|
||||
(if (null? s)
|
||||
r
|
||||
(if (eq? (caar s) k)
|
||||
(loop (cdr s) r)
|
||||
(loop (cdr s) (cons (car s) r)))))))
|
||||
(cons (cons k v)
|
||||
dr)))
|
||||
|
||||
;; Returns the list of keys stored in given dictionary.
|
||||
(define (dict-keys d)
|
||||
(map car d))
|
||||
|
||||
;; Maps dictionary values, the procedure gets key-value pairs if it
|
||||
;; accepts more than one argument. If it accepts a third argument,
|
||||
;; index gets passed as well.
|
||||
(define (dict-map proc d)
|
||||
(let* ((lpi (length (procedure-information proc)))
|
||||
(both? (> lpi 2))
|
||||
(index? (> lpi 3)))
|
||||
(let loop ((d d)
|
||||
(r '())
|
||||
(i 0))
|
||||
(if (null? d)
|
||||
r ; No reverse needed, order does not matter
|
||||
(loop (cdr d)
|
||||
(let ((k (caar d))
|
||||
(v (cdar d)))
|
||||
(cons (cons k (if both?
|
||||
(if index?
|
||||
(proc k v i)
|
||||
(proc k v))
|
||||
(proc v)))
|
||||
r))
|
||||
(add1 i))))))
|
||||
|
||||
;; Returns a dictionary containing only kv pairs matching the
|
||||
;; predicate which must accept two arguments. Unlike list filter,
|
||||
;; does not perform final reverse on the result.
|
||||
(define (dict-filter pred? d)
|
||||
(let loop ((d d)
|
||||
(r '()))
|
||||
(if (null? d)
|
||||
r
|
||||
(loop (cdr d)
|
||||
(if (pred? (caar d) (cdar d))
|
||||
(cons (car d) r)
|
||||
r)))))
|
||||
|
||||
;; Reduce over dictinary, the reducing procedure gets accumulator,
|
||||
;; key and value as its three arguments.
|
||||
(define (dict-reduce init proc d)
|
||||
(let loop ((d d)
|
||||
(acc init))
|
||||
(if (null? d)
|
||||
acc
|
||||
(loop (cdr d)
|
||||
(proc acc (caar d) (cdar d))))))
|
||||
|
||||
;; Performs self-tests of the dictionary module.
|
||||
(define (dictionary-tests!)
|
||||
(run-tests
|
||||
dict
|
||||
(test-true make-dict (null? (make-dict)))
|
||||
(test-exn dict-ref (dict-ref (make-dict) 'nonexistent))
|
||||
(test-true dict-ref (dict-ref (make-dict) 'nonexistent #t))
|
||||
(test-equal? dict-set (dict-set (make-dict) 'nonexistent 1) '((nonexistent . 1)))
|
||||
(test-equal? dict-set (dict-set (dict-set (make-dict) 'existent 1) 'existent 2) '((existent . 2)))
|
||||
(test-exn dict-remove (dict-remove (make-dict) 'nonexistent))
|
||||
(test-true dict-remove (null? (dict-remove (dict-set (make-dict) 'existing 1) 'existing)))
|
||||
(test-equal? dict-keys (dict-keys (dict-set (make-dict) 'existing 1)) '(existing))
|
||||
(test-equal? dict-map (dict-map (lambda (v) (* 2 v))
|
||||
'((a . 1)
|
||||
(b . 2)))
|
||||
'((a . 2)
|
||||
(b . 4)))
|
||||
(test-equal? dict-map (dict-map (lambda (k v) (* 2 v))
|
||||
'((a . 1)
|
||||
(b . 2)))
|
||||
'((a . 2)
|
||||
(b . 4)))
|
||||
(test-equal? dict-filter (dict-filter (lambda (k v)
|
||||
(odd? v))
|
||||
'((a . 1)
|
||||
(b . 2)))
|
||||
'((a . 1)))
|
||||
(test-eq? dict-reduce
|
||||
(dict-reduce 0 (lambda (a k v) (+ a v)) '((a . 1) (b . 2)))
|
||||
3)
|
||||
))
|
||||
|
||||
)
|
203
src/listing.scm
Normal file
203
src/listing.scm
Normal file
|
@ -0,0 +1,203 @@
|
|||
;;
|
||||
;; listing.scm
|
||||
;;
|
||||
;; Source listing with line highlights.
|
||||
;;
|
||||
;; ISC License
|
||||
;;
|
||||
;; Copyright 2023 Brmlab, z.s.
|
||||
;; Dominik Pantůček <dominik.pantucek@trustica.cz>
|
||||
;;
|
||||
;; 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 listing))
|
||||
|
||||
(module
|
||||
listing
|
||||
(
|
||||
print-source-listing
|
||||
listing-tests!
|
||||
)
|
||||
|
||||
(import scheme
|
||||
(chicken base)
|
||||
(chicken string)
|
||||
(chicken format)
|
||||
(chicken keyword)
|
||||
testing
|
||||
ansi)
|
||||
|
||||
;; Returns the number of digits required to represent a given number
|
||||
;; in decimal format.
|
||||
(define (number-digits number)
|
||||
(let loop ((number (abs number))
|
||||
(digits 0))
|
||||
(if (= number 0)
|
||||
(if (= digits 0)
|
||||
1
|
||||
digits)
|
||||
(loop (quotient number 10)
|
||||
(+ digits 1)))))
|
||||
|
||||
;; Formats line number padding it with spaces from the left for
|
||||
;; alignment with number of given maximum number of digits and
|
||||
;; appends ordinal dot and space.
|
||||
(define (format-line-number number digits)
|
||||
(let loop ((args (list (sprintf "~A. " number)))
|
||||
(spaces (- digits (number-digits number))))
|
||||
(if (<= spaces 0)
|
||||
(string-intersperse args "")
|
||||
(loop (cons " " args)
|
||||
(- spaces 1)))))
|
||||
|
||||
;; Returns true if given line is near the target line.
|
||||
(define (line-near-target? line target context)
|
||||
(let ((target-line (if (list? target)
|
||||
(car target)
|
||||
target)))
|
||||
(or (< context 0)
|
||||
(<= (abs (- line target-line)) context))))
|
||||
|
||||
;; Returns true if given line is near one of the target lines given.
|
||||
(define (line-near-targets? line targets context)
|
||||
(let loop ((targets targets))
|
||||
(if (null? targets)
|
||||
(< context 0)
|
||||
(if (line-near-target? line (car targets) context)
|
||||
#t
|
||||
(loop (cdr targets))))))
|
||||
|
||||
;; Returns true if given number is in highlights.
|
||||
(define (in-highlights? number highlights)
|
||||
(let loop ((highlights highlights))
|
||||
(if (null? highlights)
|
||||
#f
|
||||
(let* ((highlight (car highlights))
|
||||
(line-number (if (list? highlight)
|
||||
(car highlight)
|
||||
highlight)))
|
||||
(if (= number line-number)
|
||||
#t
|
||||
(loop (cdr highlights)))))))
|
||||
|
||||
;; Returns hihglight match - each highlight should contain:
|
||||
;; line-number, message, stage, type (error, warning, info ...)
|
||||
(define (match-highlight number highlights)
|
||||
(let loop ((highlights highlights))
|
||||
(if (null? highlights)
|
||||
#f
|
||||
(let* ((highlight (car highlights))
|
||||
(line-number (if (list? highlight)
|
||||
(car highlight)
|
||||
highlight)))
|
||||
(if (= number line-number)
|
||||
highlight
|
||||
(loop (cdr highlights)))))))
|
||||
|
||||
;; Returns comment if there is any
|
||||
(define (highlight-comment number highlights)
|
||||
(let loop ((highlights highlights))
|
||||
(if (null? highlights)
|
||||
#f
|
||||
(let* ((highlight (car highlights))
|
||||
(line-number (if (list? highlight)
|
||||
(car highlight)
|
||||
highlight)))
|
||||
(if (= number line-number)
|
||||
(if (list? highlight)
|
||||
(cadr highlight)
|
||||
#f)
|
||||
(loop (cdr highlights)))))))
|
||||
|
||||
;; Prints and highlights a selection of source listing lines and
|
||||
;; their optional context.
|
||||
(define (print-source-listing lines highlights . args)
|
||||
(let ((highlight-rules (get-keyword #:highlight-rules args
|
||||
(lambda ()
|
||||
`((error ,(ansi #:bold #:red) ,(ansi #:default))
|
||||
(warning ,(ansi #:yellow) ,(ansi #:default))
|
||||
(info ,(ansi #:cyan) ,(ansi #:default))
|
||||
))))
|
||||
(ellipsis (get-keyword #:ellipsis args (lambda () "...")))
|
||||
(ctx-pre (get-keyword #:context-pre args (lambda () "")))
|
||||
(ctx-post (get-keyword #:context-post args (lambda () "")))
|
||||
(hl-pre (get-keyword #:highlight-pre args (lambda () a:error)))
|
||||
(hl-post (get-keyword #:highlight-post args (lambda () a:default)))
|
||||
(context (get-keyword #:context args (lambda () 3))))
|
||||
(let ((digits (number-digits (length lines))))
|
||||
(let loop ((lines lines)
|
||||
(number 1)
|
||||
(printed-something #f)
|
||||
(was-printing #f))
|
||||
(when (not (null? lines))
|
||||
(let* ((highlight (match-highlight number highlights))
|
||||
(hl-type (if highlight (cadddr highlight) #f))
|
||||
(hl-def (assq hl-type highlight-rules))
|
||||
(hl-pre-real (if hl-def (cadr hl-def) hl-pre))
|
||||
(hl-post-real (if hl-def (caddr hl-def) hl-post))
|
||||
(context? (and (not highlight)
|
||||
(line-near-targets? number highlights context)))
|
||||
(print? (or highlight context?)))
|
||||
(cond (print?
|
||||
(when (and printed-something
|
||||
(not was-printing))
|
||||
(print ellipsis))
|
||||
(if highlight
|
||||
(display hl-pre-real)
|
||||
(when context?
|
||||
(display ctx-pre)))
|
||||
(display (sprintf "~A~A~A"
|
||||
(format-line-number number digits)
|
||||
(car lines)
|
||||
(let ((comment (highlight-comment number highlights)))
|
||||
(if comment
|
||||
(sprintf " # <<< ~A" comment)
|
||||
""))))
|
||||
(if highlight
|
||||
(display hl-post-real)
|
||||
(when context?
|
||||
(display ctx-post)))
|
||||
(newline)
|
||||
(loop (cdr lines)
|
||||
(+ number 1)
|
||||
#t
|
||||
#t))
|
||||
(else
|
||||
(loop (cdr lines)
|
||||
(+ number 1)
|
||||
printed-something
|
||||
#f)))))))))
|
||||
|
||||
;; Performs self-tests of the listing module.
|
||||
(define (listing-tests!)
|
||||
(run-tests
|
||||
listing
|
||||
(test-eq? number-digits (number-digits 0) 1)
|
||||
(test-eq? number-digits (number-digits 1) 1)
|
||||
(test-eq? number-digits (number-digits 9) 1)
|
||||
(test-eq? number-digits (number-digits 10) 2)
|
||||
(test-eq? number-digits (number-digits 999) 3)
|
||||
(test-equal? format-line-number (format-line-number 5 3) " 5. ")
|
||||
(test-true line-near-target? (line-near-target? 4 5 -1))
|
||||
(test-true line-near-target? (line-near-target? 4 5 1))
|
||||
(test-true line-near-target? (line-near-target? 1 5 10))
|
||||
(test-false line-near-target? (line-near-target? 4 5 0))
|
||||
(test-true line-near-targets? (line-near-targets? 4 '(1 5 10) 1))
|
||||
(test-false line-near-targets? (line-near-targets? 3 '(1 5 10) 1))
|
||||
(test-true line-near-targets? (line-near-targets? 3 '(1 5 10) 2))
|
||||
))
|
||||
|
||||
)
|
185
src/member-fees.scm
Normal file
185
src/member-fees.scm
Normal file
|
@ -0,0 +1,185 @@
|
|||
;;
|
||||
;; member-fees.scm
|
||||
;;
|
||||
;; Member fees manipulation.
|
||||
;;
|
||||
;; ISC License
|
||||
;;
|
||||
;; Copyright 2023 Brmlab, z.s.
|
||||
;; Dominik Pantůček <dominik.pantucek@trustica.cz>
|
||||
;;
|
||||
;; 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 member-fees))
|
||||
|
||||
(module
|
||||
member-fees
|
||||
(
|
||||
member-calendar
|
||||
member-calendar-first-month
|
||||
member-calendar-last-month
|
||||
member-calendar-query
|
||||
member-calendar->years-table
|
||||
member-calendar->fees
|
||||
member-fees-total
|
||||
member-credit-total
|
||||
member-calendar->table
|
||||
)
|
||||
|
||||
(import scheme
|
||||
(chicken base)
|
||||
(chicken format)
|
||||
(chicken sort)
|
||||
configuration
|
||||
member-record
|
||||
month
|
||||
ansi
|
||||
table
|
||||
members-base
|
||||
period)
|
||||
|
||||
;; Convert into lookups - a list of (list period regular student)
|
||||
(define member-fees-lookup-table
|
||||
(make-period-lookup-table
|
||||
'(((2010 1) 500 250))))
|
||||
|
||||
;; Returns a matching list of (list regular student)
|
||||
(define (lookup-member-fees)
|
||||
(lookup-by-period member-fees-lookup-table))
|
||||
|
||||
;; Returns time-based fee for given type
|
||||
(define (lookup-member-fee type)
|
||||
(let ((fees (lookup-member-fees)))
|
||||
(if (eq? type 'student)
|
||||
(cadr fees)
|
||||
(car fees))))
|
||||
|
||||
;; Returns a list of months where each month is a list containing:
|
||||
;; * month (from month module)
|
||||
;; * flags - a list of symbols: student, suspended, destroyed
|
||||
;; The list contains all months from 'joined until (*current-month*).
|
||||
(define (member-calendar mr . args)
|
||||
(let ((last-month (if (null? args)
|
||||
(*current-month*)
|
||||
(car args)))
|
||||
(first-month (period-since (car (member-record-info mr 'member)))))
|
||||
(let loop ((cm first-month)
|
||||
(cal '()))
|
||||
(if (month>? cm last-month)
|
||||
(reverse cal)
|
||||
(loop (month-add cm)
|
||||
(cons (list cm
|
||||
(parameterize ((*current-month* cm))
|
||||
(member-flags mr)))
|
||||
cal))))))
|
||||
|
||||
;; Returns the first month of the calendar
|
||||
(define (member-calendar-first-month mc)
|
||||
(caar mc))
|
||||
|
||||
;; Returns the last month of the calendar
|
||||
(define (member-calendar-last-month mc)
|
||||
(caar (reverse mc)))
|
||||
|
||||
;; Returns the calendar entry which matches given month or #f if none
|
||||
;; found.
|
||||
(define (member-calendar-query mc m)
|
||||
(assoc m mc))
|
||||
|
||||
;; Formats the calendar entry for visualization
|
||||
(define (member-calendar-entry->string e)
|
||||
(if e
|
||||
(if (member 'existing (cadr e))
|
||||
(if (member 'suspended (cadr e))
|
||||
(ansi-string #:bgdarkgrey " ") ; Suspended
|
||||
(if (member 'destroyed (cadr e))
|
||||
(ansi-string #:bgblack "~~") ; Destroyed
|
||||
(if (member 'student (cadr e))
|
||||
(ansi-string #:bgyellow " ") ; Student
|
||||
(ansi-string #:bggreen " ")))) ; Normal
|
||||
" ") ; Nonexistent - should not happen
|
||||
" ")) ; Nonexistent
|
||||
|
||||
;; Converts the entry into the fee
|
||||
(define (member-calendar-entry->fee e)
|
||||
(if e
|
||||
(if (member 'existing (cadr e))
|
||||
(if (member 'suspended (cadr e))
|
||||
0 ; Suspended
|
||||
(if (member 'destroyed (cadr e))
|
||||
0 ; Destroyed
|
||||
(if (member 'student (cadr e))
|
||||
(lookup-member-fee 'student) ; Student
|
||||
(lookup-member-fee 'regular)))) ; Normal
|
||||
0) ; Nonexistent - should not happen
|
||||
0)) ; Nonexistent
|
||||
|
||||
;; Converts the calendar into a table where rows represent years and
|
||||
;; contain the year in the first cell and 12 cells for months after
|
||||
;; it.
|
||||
(define (member-calendar->years-table mc)
|
||||
(if (null? mc)
|
||||
'()
|
||||
(let* ((fm (member-calendar-first-month mc))
|
||||
(lm (member-calendar-last-month mc))
|
||||
(fy (month-year fm))
|
||||
(ly (month-year lm)))
|
||||
(let loop ((y fy)
|
||||
(rows '()))
|
||||
(if (> y ly)
|
||||
(reverse rows)
|
||||
(loop (add1 y)
|
||||
(cons (let mloop ((m 1)
|
||||
(row (list y)))
|
||||
(if (> m 12)
|
||||
(reverse row)
|
||||
(mloop (add1 m)
|
||||
(cons (member-calendar-entry->string
|
||||
(member-calendar-query mc (make-month y m)))
|
||||
row))))
|
||||
rows)))))))
|
||||
|
||||
;; Converts the whole calendar into a list of amounts (fees)
|
||||
(define (member-calendar->fees mc)
|
||||
(map member-calendar-entry->fee mc))
|
||||
|
||||
;; Returns the total sum of fees for all months relevant for given
|
||||
;; member
|
||||
(define (member-fees-total mr)
|
||||
(foldl + 0 (member-calendar->fees
|
||||
(member-calendar mr))))
|
||||
|
||||
;; Total credit manually recorded in member record
|
||||
(define (member-credit-total mr)
|
||||
(let* ((credit (member-record-info mr 'credit '()))
|
||||
(amounts (map car credit)))
|
||||
(foldl + 0 amounts)))
|
||||
|
||||
;; Nicely print calendar for given member
|
||||
(define (member-calendar->table mr)
|
||||
(let* ((mc (member-calendar mr))
|
||||
(fees (member-calendar->fees mc)))
|
||||
(table->string (cons (map (lambda (c)
|
||||
(sprintf "\t~A\t" c))
|
||||
(list "" 1 2 3 4 5 6 7 8 9 10 11 12))
|
||||
(member-calendar->years-table mc))
|
||||
#:table-border #f
|
||||
#:row-border #t
|
||||
#:col-border #t
|
||||
#:ansi #t
|
||||
)))
|
||||
|
||||
)
|
292
src/member-parser.scm
Normal file
292
src/member-parser.scm
Normal file
|
@ -0,0 +1,292 @@
|
|||
;;
|
||||
;; member-parser.scm
|
||||
;;
|
||||
;; Member file parsing.
|
||||
;;
|
||||
;; ISC License
|
||||
;;
|
||||
;; Copyright 2023 Brmlab, z.s.
|
||||
;; Dominik Pantůček <dominik.pantucek@trustica.cz>
|
||||
;;
|
||||
;; 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 member-parser))
|
||||
|
||||
(module
|
||||
member-parser
|
||||
(
|
||||
load-member-file
|
||||
member-parser-tests!
|
||||
)
|
||||
|
||||
(import scheme
|
||||
(chicken base)
|
||||
(chicken io)
|
||||
(chicken irregex)
|
||||
member-record
|
||||
testing
|
||||
dictionary
|
||||
month
|
||||
period
|
||||
utils
|
||||
configuration)
|
||||
|
||||
;; Pass 2: known keys
|
||||
(define mandatory-keys '(nick name mail phone))
|
||||
(define optional-keys '(born))
|
||||
(define known-multikeys
|
||||
'(card desfire
|
||||
credit
|
||||
studentstart studentstop
|
||||
suspendstart suspendstop
|
||||
joined destroyed))
|
||||
(define ignored-keys '(mail2))
|
||||
|
||||
(define known-keys (append mandatory-keys optional-keys))
|
||||
|
||||
;; Dynamic start/stop markers
|
||||
(define start-stop-markers-lookup
|
||||
'(
|
||||
(studentstart student start)
|
||||
(studentstop student stop)
|
||||
(suspendstart suspend start)
|
||||
(suspendstop suspend stop)
|
||||
|
||||
(joined member start)
|
||||
(destroyed member stop)
|
||||
))
|
||||
(define start-stop-markers (map car start-stop-markers-lookup))
|
||||
|
||||
;; Pass 3: Interpreter passes
|
||||
(define member-schema-interpreters
|
||||
`((pass-markers
|
||||
,(lambda (mr output key value)
|
||||
(if (member key start-stop-markers)
|
||||
(let* ((mk (assq key start-stop-markers-lookup))
|
||||
(marker (caddr mk))
|
||||
(kind (cadr mk)))
|
||||
(foldl (lambda (mr value)
|
||||
(let* ((mspec (string-first+rest (car value)))
|
||||
(month (string->month (car mspec)))
|
||||
(comment (cdr mspec)))
|
||||
(if month
|
||||
(member-record-sub-prepend
|
||||
mr output kind
|
||||
(list marker month (cdr value) comment))
|
||||
(member-record-add-highlight
|
||||
mr (cdr value) "Invalid month specification" 3 'error))))
|
||||
mr value))
|
||||
(member-record-sub-set mr output key value))))
|
||||
(info
|
||||
,(lambda (mr output key value)
|
||||
(case key
|
||||
((student suspend member)
|
||||
(let* ((res (period-markers->periods value))
|
||||
(ok? (car res))
|
||||
(periods (cadr res))
|
||||
(msg (caddr res))
|
||||
(line-number (cadddr res))
|
||||
(mr1 (member-record-sub-set mr output key periods)))
|
||||
(if ok?
|
||||
mr1
|
||||
(member-record-add-highlight mr1 line-number msg 3 'error))))
|
||||
((card desfire)
|
||||
(member-record-sub-set mr output key
|
||||
(map
|
||||
(lambda (rec)
|
||||
(string-first+rest (car rec)))
|
||||
value)))
|
||||
((credit)
|
||||
(member-record-sub-set mr output key
|
||||
(map
|
||||
(lambda (rec)
|
||||
(let* ((fr (string-first+rest (car rec)))
|
||||
(amt (string->number (car fr)))
|
||||
(msg (cdr fr)))
|
||||
(cons amt msg)))
|
||||
value)))
|
||||
((nick)
|
||||
(let ((mr0 (member-record-sub-set mr output key (car value))))
|
||||
(if (irregex-search (irregex "[ \\t]" 'u) (car value))
|
||||
(member-record-add-highlight
|
||||
mr0 (cdr value) "Whitespace not allowed in nick" 3 'error)
|
||||
mr0)))
|
||||
(else
|
||||
(member-record-sub-set mr output key (car value))))))))
|
||||
|
||||
;; Pass 4: Final checks - add defaults
|
||||
(define (member-schema-finalize mr)
|
||||
(apply
|
||||
member-record-sub-ensure
|
||||
mr 'info
|
||||
(join (map (lambda (mk) (list mk #f)) mandatory-keys))))
|
||||
|
||||
;; Pass 0: Removes any comments and removes any leading and trailing
|
||||
;; whitespace.
|
||||
(define (preprocess-member-line line)
|
||||
(irregex-replace (irregex "[ \\t]*$" 'u)
|
||||
(irregex-replace (irregex "^[ \\t]*" 'u)
|
||||
(irregex-replace (irregex "#.*$" 'u) line "")
|
||||
"")
|
||||
""))
|
||||
|
||||
;; Pass 1: Expects line with comments and surrounding whitespace
|
||||
;; removed, returns either #f if nothing was parsed, symbol if only
|
||||
;; one token was there and pair of symbol and string if both key and
|
||||
;; the value were present.
|
||||
(define (parse-member-line line)
|
||||
(if (= (string-length line) 0)
|
||||
#f
|
||||
(let ((dm (irregex-search (irregex "[ \\t]" 'u) line)))
|
||||
(if dm
|
||||
(let* ((sep-idx (irregex-match-start-index dm))
|
||||
(key-str (substring line 0 sep-idx))
|
||||
(key (string->symbol key-str))
|
||||
(sep+val (substring line sep-idx))
|
||||
(val (irregex-replace (irregex "^[ \\t]*" 'u) sep+val "")))
|
||||
(cons key val))
|
||||
(string->symbol line)))))
|
||||
|
||||
;; Passes 0 and 1: Adds parsed lines to member record.
|
||||
(define (parse-member-lines mr source)
|
||||
(let loop ((lines source)
|
||||
(mr (member-record-set mr #:source source))
|
||||
(result '())
|
||||
(line-number 1))
|
||||
(if (null? lines)
|
||||
(member-record-set mr #:parsed (reverse result))
|
||||
(let ((parsed-line (parse-member-line
|
||||
(preprocess-member-line
|
||||
(car lines)))))
|
||||
(loop (cdr lines)
|
||||
(if (symbol? parsed-line)
|
||||
(member-record-add-highlight mr line-number "Got only key" 1 'error)
|
||||
mr)
|
||||
(if (pair? parsed-line)
|
||||
(cons (list (car parsed-line)
|
||||
(cdr parsed-line)
|
||||
line-number)
|
||||
result)
|
||||
result)
|
||||
(add1 line-number))))))
|
||||
|
||||
;; Pass 2: Converts parsed key/value/line records into a proper
|
||||
;; dictionary. Known keys are stored as pairs of value and line
|
||||
;; number, known multikeys as lists of pairs of value and line
|
||||
;; number.
|
||||
(define (process-member-file mr)
|
||||
(let loop ((parsed (dict-ref mr 'parsed))
|
||||
(mr mr)
|
||||
(processed (make-dict)))
|
||||
(if (null? parsed)
|
||||
(member-record-set mr #:processed processed)
|
||||
(let* ((line (car parsed))
|
||||
(key (car line))
|
||||
(value (cadr line))
|
||||
(number (caddr line)))
|
||||
(if (member key known-keys)
|
||||
(if (dict-has-key? processed key)
|
||||
(loop (cdr parsed)
|
||||
(member-record-add-highlight mr number "Duplicate key" 2 'error)
|
||||
processed)
|
||||
(loop (cdr parsed)
|
||||
mr
|
||||
(dict-set processed key (cons value number))))
|
||||
(if (member key known-multikeys)
|
||||
(loop (cdr parsed)
|
||||
mr
|
||||
(dict-set processed key (cons (cons value number)
|
||||
(dict-ref processed key '()))))
|
||||
(loop (cdr parsed)
|
||||
(if (member key ignored-keys)
|
||||
mr
|
||||
(member-record-add-highlight mr number "Unknown key" 2 'warning))
|
||||
processed)))))))
|
||||
|
||||
;; Pass 3+: Single interpreter pass - input must be
|
||||
;; dictionary. Output is top-level key of member record.
|
||||
(define (interpreter-pass mr output input pass-proc)
|
||||
(let loop ((keys (dict-keys input))
|
||||
(mr (dict-set mr output (make-dict))))
|
||||
(if (null? keys)
|
||||
mr
|
||||
(let ((key (car keys)))
|
||||
(loop (cdr keys)
|
||||
(pass-proc mr output key
|
||||
(dict-ref input key)))))))
|
||||
|
||||
;; Pass 3+: Interpreter passes
|
||||
(define (interpret-member-file mr . starts)
|
||||
(let ((input-name (if (null? starts)
|
||||
'processed
|
||||
(car starts))))
|
||||
(let loop ((passes member-schema-interpreters)
|
||||
(prev-name input-name)
|
||||
(mr mr))
|
||||
(if (null? passes)
|
||||
mr
|
||||
(let* ((pass (car passes))
|
||||
(pass-name (car pass))
|
||||
(pass-proc (cadr pass)))
|
||||
(loop (cdr passes)
|
||||
(caar passes)
|
||||
(interpreter-pass mr pass-name (dict-ref mr prev-name) pass-proc)))))))
|
||||
|
||||
;; Loads member file source. Performs passes 0, 1 and 2.
|
||||
(define (load-member-file mr)
|
||||
(let* ((mrif (member-record-input-file mr))
|
||||
(source (read-lines mrif))
|
||||
(mrp (parse-member-lines mr source)))
|
||||
(member-schema-finalize
|
||||
(interpret-member-file
|
||||
(process-member-file mrp)))))
|
||||
|
||||
;; Performs self-tests of the member-parser module.
|
||||
(define (member-parser-tests!)
|
||||
(run-tests
|
||||
member-parser
|
||||
(test-equal? preprocess-member-line
|
||||
(preprocess-member-line "# all comment")
|
||||
"")
|
||||
(test-equal? preprocess-member-line
|
||||
(preprocess-member-line " # all comment after spaces")
|
||||
"")
|
||||
(test-equal? preprocess-member-line
|
||||
(preprocess-member-line " test # spaces and comment after spaces")
|
||||
"test")
|
||||
(test-equal? preprocess-member-line
|
||||
(preprocess-member-line "key value # spaces and comment after spaces")
|
||||
"key value")
|
||||
(test-false parse-member-line
|
||||
(parse-member-line ""))
|
||||
(test-eq? parse-member-line
|
||||
(parse-member-line "key")
|
||||
'key)
|
||||
(test-equal? parse-member-line
|
||||
(parse-member-line "key value")
|
||||
'(key . "value"))
|
||||
(test-equal? parse-member-line
|
||||
(parse-member-line "key value")
|
||||
'(key . "value"))
|
||||
(test-equal? parse-member-line
|
||||
(parse-member-line "key value and some")
|
||||
'(key . "value and some"))
|
||||
(test-equal? parse-member-line
|
||||
(parse-member-line "key value lot of spaces")
|
||||
'(key . "value lot of spaces"))
|
||||
))
|
||||
|
||||
)
|
402
src/member-record.scm
Normal file
402
src/member-record.scm
Normal file
|
@ -0,0 +1,402 @@
|
|||
;;
|
||||
;; member-record.scm
|
||||
;;
|
||||
;; Procedures working with complete member record (as loaded by the
|
||||
;; members-base).
|
||||
;;
|
||||
;; ISC License
|
||||
;;
|
||||
;; Copyright 2023 Brmlab, z.s.
|
||||
;; Dominik Pantůček <dominik.pantucek@trustica.cz>
|
||||
;;
|
||||
;; 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 member-record))
|
||||
|
||||
(module
|
||||
member-record
|
||||
(
|
||||
make-member-record
|
||||
|
||||
member-file-path
|
||||
member-record-input-file
|
||||
|
||||
member-record-set
|
||||
member-record-add-highlight
|
||||
member-record-sub-ref
|
||||
member-record-sub-set
|
||||
member-record-sub-prepend
|
||||
member-record-sub-has-key?
|
||||
member-record-sub-ensure
|
||||
|
||||
member-record-info
|
||||
member-missing-keys
|
||||
member-has-highlights?
|
||||
member-record-usable?
|
||||
member-has-problems?
|
||||
|
||||
member-destroyed?
|
||||
member-suspended?
|
||||
member-active?
|
||||
member-student?
|
||||
member-existing?
|
||||
member-flags
|
||||
|
||||
member-nick
|
||||
member-id
|
||||
member-suspended-months
|
||||
|
||||
member-format
|
||||
|
||||
member<?
|
||||
|
||||
member-record-add-payment
|
||||
member-payments
|
||||
member-balance
|
||||
|
||||
member-record-tests!
|
||||
)
|
||||
|
||||
(import scheme
|
||||
(chicken base)
|
||||
(chicken keyword)
|
||||
(chicken irregex)
|
||||
(chicken string)
|
||||
(chicken format)
|
||||
dictionary
|
||||
testing
|
||||
month
|
||||
period
|
||||
configuration
|
||||
primes
|
||||
utils)
|
||||
|
||||
;; Checks whether given string is a 4-digit decimal number.
|
||||
(define (is-4digit-string? s)
|
||||
(if (irregex-search (irregex "^[0-9]{4}$") s)
|
||||
#t
|
||||
#f))
|
||||
|
||||
;; checks whether given symbol is a 4-digit one.
|
||||
(define (is-4digit-symbol? s)
|
||||
(is-4digit-string?
|
||||
(symbol->string s)))
|
||||
|
||||
;; Returns the first 4-digit symbol from the list.
|
||||
(define (get-4digit-symbol-from-list lst)
|
||||
(let loop ((lst lst))
|
||||
(if (null? lst)
|
||||
#f
|
||||
(if (is-4digit-symbol? (car lst))
|
||||
(car lst)
|
||||
(loop (cdr lst))))))
|
||||
|
||||
;; Creates new member record based on the file and symlinks
|
||||
;; information received from the members directory. Any keyword
|
||||
;; arguments are converted to respective symbols in the dictionary.
|
||||
(define (make-member-record file-name file-path symlinks . args)
|
||||
(let loop ((args args)
|
||||
(pairs `((file-name . ,file-name)
|
||||
(file-path . ,file-path)
|
||||
(symlinks . ,symlinks)
|
||||
(id . ,(string->number
|
||||
(symbol->string
|
||||
(get-4digit-symbol-from-list (cons file-name symlinks))))))))
|
||||
(if (null? args)
|
||||
(make-dict pairs)
|
||||
(if (not (keyword? (car args)))
|
||||
(error 'make-member-record "Optional arguments must be keywords" (car args))
|
||||
(if (null? (cdr args))
|
||||
(error 'make-member-record "Each optional keyword argument must have a value" (car args))
|
||||
(loop (cddr args)
|
||||
(cons (cons (string->symbol (keyword->string (car args)))
|
||||
(cadr args))
|
||||
pairs)))))))
|
||||
|
||||
;; Convenience accessor
|
||||
(define (member-file-path mr)
|
||||
(dict-ref mr 'file-path))
|
||||
|
||||
;; Returns opened input file for this record (used by parser).
|
||||
(define (member-record-input-file mr)
|
||||
(open-input-file
|
||||
(dict-ref mr 'file-path)))
|
||||
|
||||
;; Sets pairs of keys/values for given member record.
|
||||
(define (member-record-set mr . args)
|
||||
(let loop ((args args)
|
||||
(mr mr))
|
||||
(if (null? args)
|
||||
mr
|
||||
(if (not (keyword? (car args)))
|
||||
(error 'member-record-set "Needs argument keyword" (car args))
|
||||
(if (null? (cdr args))
|
||||
(error 'member-record-set "Argument needs value" (car args))
|
||||
(loop (cddr args)
|
||||
(dict-set mr (string->symbol (keyword->string (car args)))
|
||||
(cadr args))))))))
|
||||
|
||||
;; Adds highlight identified by line number, message, pass number and
|
||||
;; type (error, warning, info).
|
||||
(define (member-record-add-highlight mr line-number message pass type)
|
||||
(dict-set mr 'highlights
|
||||
(cons (list line-number message pass type)
|
||||
(dict-ref mr 'highlights '()))))
|
||||
|
||||
;; Returns a key from particular section
|
||||
(define (member-record-sub-ref mr sec key . defaults)
|
||||
(let ((sec-dict (dict-ref mr sec)))
|
||||
(if (null? defaults)
|
||||
(dict-ref sec-dict key)
|
||||
(dict-ref sec-dict key (car defaults)))))
|
||||
|
||||
;; Sets a key in particular section
|
||||
(define (member-record-sub-set mr sec key val)
|
||||
(let ((sec-dict (dict-ref mr sec)))
|
||||
(dict-set mr sec
|
||||
(dict-set sec-dict key val))))
|
||||
|
||||
;; Prepends value to given subkey
|
||||
(define (member-record-sub-prepend mr sec key val)
|
||||
(member-record-sub-set mr sec key
|
||||
(cons val
|
||||
(member-record-sub-ref mr sec key '()))))
|
||||
|
||||
;; Returns true if given section contains given key
|
||||
(define (member-record-sub-has-key? mr sec key)
|
||||
(dict-has-key? (dict-ref mr sec) key))
|
||||
|
||||
;; Returns new member record with section updated by defaults, the
|
||||
;; section must already exist.
|
||||
(define (member-record-sub-ensure mr sec . kvs)
|
||||
(let loop ((kvs kvs)
|
||||
(sd (dict-ref mr sec)))
|
||||
(if (null? kvs)
|
||||
(dict-set mr sec sd)
|
||||
(if (null? (cdr kvs))
|
||||
(error 'member-record-sub-ensure "Needs pairs of keys and values" kvs)
|
||||
(let ((key (car kvs))
|
||||
(val (cadr kvs)))
|
||||
(loop (cddr kvs)
|
||||
(if (dict-has-key? sd key)
|
||||
sd
|
||||
(dict-set sd key val))))))))
|
||||
|
||||
;; Returns member info key value
|
||||
(define (member-record-info mr key . defaults)
|
||||
(let ((info (dict-ref mr 'info)))
|
||||
(if (null? defaults)
|
||||
(dict-ref info key)
|
||||
(dict-ref info key (car defaults)))))
|
||||
|
||||
;; Return mandatory keys with #f as value
|
||||
(define (member-missing-keys mr)
|
||||
(dict-reduce '()
|
||||
(lambda (acc k v)
|
||||
(if v acc (cons k acc)))
|
||||
(dict-ref mr 'info)))
|
||||
|
||||
;; True if there are any source highlights
|
||||
(define (member-has-highlights? mr)
|
||||
(dict-has-key? mr 'highlights))
|
||||
|
||||
;; Returns true if there is at least one highlight of given type
|
||||
(define (member-highlights-has-type? mr type)
|
||||
(let loop ((hls (dict-ref mr 'highlights '())))
|
||||
(if (null? hls)
|
||||
#f
|
||||
(if (eq? (cadddr (car hls)) type)
|
||||
#t
|
||||
(loop (cdr hls))))))
|
||||
|
||||
;; Returns true if there is at least one highlight with error type
|
||||
(define (member-has-errors? mr)
|
||||
(member-highlights-has-type? mr 'error))
|
||||
|
||||
;; Absolutely required
|
||||
(define (member-record-usable? mr)
|
||||
(dict-has-key? (dict-ref mr 'info) 'member))
|
||||
|
||||
;; True if member record is OK
|
||||
(define (member-has-problems? mr)
|
||||
(or (member-has-errors? mr)
|
||||
(not (member-record-usable? mr))
|
||||
(not (is-4digit-prime? (member-id mr)))))
|
||||
|
||||
;; Returns true if the member record represents non-existing
|
||||
;; member. The *current-month* is a global parameter from period
|
||||
;; module.
|
||||
(define (member-destroyed? mr)
|
||||
(and (not (member-existing? mr))
|
||||
(let ((member (member-record-info mr 'member)))
|
||||
(if (null? member)
|
||||
#f
|
||||
(month>=? (*current-month*)
|
||||
(period-since (car member)))))))
|
||||
|
||||
;; Generic period-based predicate
|
||||
(define ((member-period-predicate? key) mr)
|
||||
(let ((periods (member-record-info mr key #f)))
|
||||
(and periods
|
||||
(month-in-periods? periods))))
|
||||
|
||||
;; Returns true if the member is now suspended
|
||||
(define member-is-suspended?
|
||||
(member-period-predicate? 'suspend))
|
||||
|
||||
;; Suspended must not be destroyed
|
||||
(define (member-suspended? mr)
|
||||
(and (member-is-suspended? mr)
|
||||
(not (member-destroyed? mr))))
|
||||
|
||||
;; True if the member is student
|
||||
(define member-is-student?
|
||||
(member-period-predicate? 'student))
|
||||
|
||||
;; Only active members can be students.
|
||||
(define (member-student? mr)
|
||||
(and (member-active? mr)
|
||||
(member-is-student? mr)))
|
||||
|
||||
;; Returns true if the member is active (not suspended or destroyed).
|
||||
(define (member-active? mr)
|
||||
(and (month-in-periods? (member-record-info mr 'member))
|
||||
(not (member-suspended? mr))))
|
||||
|
||||
;; Returns true if the member is currently a member
|
||||
(define (member-existing? mr)
|
||||
(month-in-periods?
|
||||
(member-record-info mr 'member)))
|
||||
|
||||
;; Returns a list of flags of given member record.
|
||||
(define (member-flags mr)
|
||||
(filter identity
|
||||
(list (if (member-student? mr) 'student #f)
|
||||
(if (member-suspended? mr) 'suspended #f)
|
||||
(if (member-active? mr) 'active #f)
|
||||
(if (member-destroyed? mr) 'destroyed #f)
|
||||
(if (member-existing? mr) 'existing #f))))
|
||||
|
||||
;; Nickname as string
|
||||
(define (member-nick mr)
|
||||
(member-record-info mr 'nick))
|
||||
|
||||
;; Returns member id
|
||||
(define (member-id mr)
|
||||
(dict-ref mr 'id))
|
||||
|
||||
;; Returns the number of months the user is suspended. Zero if not
|
||||
;; suspended.
|
||||
(define (member-suspended-months mr)
|
||||
(if (member-suspended? mr)
|
||||
(let ((period (periods-match (member-record-info mr 'suspend))))
|
||||
(if period
|
||||
(month-diff (car period) (*current-month*))
|
||||
0))
|
||||
0))
|
||||
|
||||
;; Member formatting function for general use.
|
||||
(define (member-format fmt mr)
|
||||
(let loop ((fmtl (string->list fmt))
|
||||
(resl '()))
|
||||
(if (null? fmtl)
|
||||
(string-intersperse (reverse resl) "")
|
||||
(let ((ch (car fmtl)))
|
||||
(if (eq? ch #\~)
|
||||
(loop (cddr fmtl)
|
||||
(cons (case (cadr fmtl)
|
||||
((#\N) (member-record-info mr 'nick))
|
||||
((#\I) (number->string (member-record-info mr 'id)))
|
||||
((#\S) (number->string (member-suspended-months mr)))
|
||||
((#\E)
|
||||
(let ((n (length (dict-ref mr 'highlights '()))))
|
||||
(if (eq? n 0)
|
||||
""
|
||||
(sprintf "[~A]" n))))
|
||||
((#\~) "~"))
|
||||
resl))
|
||||
(loop (cdr fmtl)
|
||||
(cons (make-string 1 (car fmtl)) resl)))))))
|
||||
|
||||
;; Comparator of member records based on nickname.
|
||||
(define (member<? a b)
|
||||
(string-ci<? (member-nick a)
|
||||
(member-nick b)))
|
||||
|
||||
;; Prepends new payment to given member record payments
|
||||
(define (member-record-add-payment mr pt)
|
||||
(dict-set mr 'payments
|
||||
(cons pt
|
||||
(dict-ref mr 'payments '()))))
|
||||
|
||||
;; Returns alist of member payments
|
||||
(define (member-payments mr)
|
||||
(dict-ref mr 'payments '()))
|
||||
|
||||
;; Balances totals
|
||||
(define (member-balance mr)
|
||||
(dict-ref mr 'balance (make-dict)))
|
||||
|
||||
;; Self-tests
|
||||
(define (member-record-tests!)
|
||||
(run-tests
|
||||
member-record
|
||||
(test-equal? make-member-record
|
||||
(make-member-record '|1234| "members/1234" '(|member|))
|
||||
'((file-name . |1234|)
|
||||
(file-path . "members/1234")
|
||||
(symlinks |member|)
|
||||
(id . 1234)))
|
||||
(test-equal? make-member-record
|
||||
(make-member-record '|1234| "members/1234" '(|member|) #:msg "msg")
|
||||
'((msg . "msg")
|
||||
(file-name . |1234|)
|
||||
(file-path . "members/1234")
|
||||
(symlinks |member|)
|
||||
(id . 1234)))
|
||||
(test-equal? member-record-set
|
||||
(member-record-set '() #:id 1234)
|
||||
'((id . 1234)))
|
||||
(test-equal? member-record-add-highlight
|
||||
(member-record-add-highlight '() 123 "Interesting..." 0 'info)
|
||||
'((highlights . ((123 "Interesting..." 0 info)))))
|
||||
(test-true member-destroyed?
|
||||
(parameterize ((*current-month* (list 2023 2)))
|
||||
(member-destroyed? '((info . ((member . (((2010 1) (2010 5))))))))))
|
||||
(test-false member-destroyed?
|
||||
(parameterize ((*current-month* (list 2009 2)))
|
||||
(member-destroyed? '((info . ((member . (((2001 1) (2010 5))))))))))
|
||||
(test-false member-suspended?
|
||||
(member-suspended? '((info . ((member . (((2015 1) #f))))))))
|
||||
(test-true member-suspended?
|
||||
(parameterize ((*current-month* (list 2015 2)))
|
||||
(member-suspended? '((info . ((member . (((2015 1) #f)))
|
||||
(suspend ((2010 1) (2022 4) #f #f))))))))
|
||||
(test-true member-suspended?
|
||||
(parameterize ((*current-month* (list 2015 2)))
|
||||
(member-suspended? '((info . ((member . (((2015 1) #f)))
|
||||
(suspend ((2010 1) #f #f #f))))))))
|
||||
(test-false member-suspended?
|
||||
(parameterize ((*current-month* (list 2023 2)))
|
||||
(member-suspended? '((info . ((member . (((2015 1) #f)))
|
||||
(suspend ((2010 1) (2022 4) #f #f))))))))
|
||||
(test-true member-active?
|
||||
(parameterize ((*current-month* (list 2023 2)))
|
||||
(member-active? '((info . ((member . (((2015 1) #f)))
|
||||
(suspend ((2010 1) (2022 4) #f #f))))))))
|
||||
))
|
||||
|
||||
)
|
200
src/members-base.scm
Normal file
200
src/members-base.scm
Normal file
|
@ -0,0 +1,200 @@
|
|||
;;
|
||||
;; members-base.scm
|
||||
;;
|
||||
;; Storage for member files.
|
||||
;;
|
||||
;; ISC License
|
||||
;;
|
||||
;; Copyright 2023 Brmlab, z.s.
|
||||
;; Dominik Pantůček <dominik.pantucek@trustica.cz>
|
||||
;;
|
||||
;; 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 members-base))
|
||||
|
||||
(module
|
||||
members-base
|
||||
(
|
||||
load-members
|
||||
find-member-by-id
|
||||
find-member-by-nick
|
||||
list-members-ids
|
||||
filter-members-by-predicate
|
||||
list-members-nicks
|
||||
members-base-info
|
||||
members-base-stats
|
||||
get-free-members-ids
|
||||
gen-member-id
|
||||
members-base-update
|
||||
)
|
||||
|
||||
(import scheme
|
||||
(chicken base)
|
||||
(chicken string)
|
||||
(chicken random)
|
||||
testing
|
||||
utils
|
||||
dictionary
|
||||
primes
|
||||
member-record
|
||||
ansi
|
||||
period
|
||||
month
|
||||
configuration
|
||||
progress
|
||||
members-dir)
|
||||
|
||||
|
||||
;; Loads members database, if the second argument is true, shows
|
||||
;; progress. Members database is a dictionary with id being the key
|
||||
;; (number) and member record being the value.
|
||||
(define (load-members dn . opts)
|
||||
(let ((progress? (and (not (null? opts))
|
||||
(car opts))))
|
||||
(with-progress%
|
||||
progress? "members"
|
||||
(let* ((fss (load-members-dir dn))
|
||||
(tot (sub1 (length (dict-keys fss))))
|
||||
(mb0 (dict-map
|
||||
(lambda (symfn symlinks prg)
|
||||
(progress%-advance (/ prg tot))
|
||||
(members-dir-load-member dn
|
||||
symfn
|
||||
symlinks))
|
||||
fss))
|
||||
(mb1 (dict-reduce (make-dict)
|
||||
(lambda (acc symfn mr)
|
||||
(dict-set acc (dict-ref mr 'id) mr))
|
||||
mb0))
|
||||
(mb (dict-reduce '()
|
||||
(lambda (acc id mr)
|
||||
(cons mr acc))
|
||||
mb1)))
|
||||
mb))))
|
||||
|
||||
;; Gets member based by generic predicate
|
||||
(define (find-member-by-predicate mb pred)
|
||||
(let loop ((mdb mb))
|
||||
(if (null? mdb)
|
||||
#f
|
||||
(let ((mr (car mdb)))
|
||||
(if (pred mr)
|
||||
mr
|
||||
(loop (cdr mdb)))))))
|
||||
|
||||
;; Returns member record found by id
|
||||
(define (find-member-by-id mb id)
|
||||
(find-member-by-predicate
|
||||
mb
|
||||
(lambda (mr)
|
||||
(eq? (dict-ref mr 'id) id))))
|
||||
|
||||
;; Returns member record found by id
|
||||
(define (find-member-by-nick mb nick)
|
||||
(find-member-by-predicate
|
||||
mb
|
||||
(lambda (mr)
|
||||
(string-ci=?
|
||||
(dict-ref
|
||||
(dict-ref mr 'info)
|
||||
'nick)
|
||||
nick))))
|
||||
|
||||
;; Returns all ids found in the database
|
||||
(define (list-members-ids mb)
|
||||
(map (lambda (mr) (dict-ref mr 'id)) mb))
|
||||
|
||||
;; Returns a list of members which match given predicate.
|
||||
(define (filter-members-by-predicate mb pred)
|
||||
(let loop ((mb mb)
|
||||
(res '()))
|
||||
(if (null? mb)
|
||||
res
|
||||
(let ((mr (car mb)))
|
||||
(loop (cdr mb)
|
||||
(if (pred mr)
|
||||
(cons mr res)
|
||||
res))))))
|
||||
|
||||
;; Returns all nicks found in the database
|
||||
(define (list-members-nicks mb)
|
||||
(map (lambda (mr) (dict-ref (dict-ref mr 'info) 'nick)) mb))
|
||||
|
||||
;; Returns dictionary with statistics about the members base.
|
||||
(define (members-base-info mb-arg)
|
||||
(let* ((mb (filter-members-by-predicate mb-arg member-record-usable?))
|
||||
(di0 (make-dict))
|
||||
(di1 (dict-set di0 'invalid
|
||||
(filter-members-by-predicate mb
|
||||
(compose not is-4digit-prime? member-id))))
|
||||
(di2 (dict-set di1 'active
|
||||
(filter-members-by-predicate mb member-active?)))
|
||||
(di3 (dict-set di2 'suspended
|
||||
(filter-members-by-predicate mb member-suspended?)))
|
||||
(di4 (dict-set di3 'students
|
||||
(filter-members-by-predicate mb member-student?)))
|
||||
(di5 (dict-set di4 'destroyed
|
||||
(filter-members-by-predicate mb member-destroyed?)))
|
||||
(di6 (dict-set di5 'month (*current-month*)))
|
||||
(di7 (dict-set di6 'total mb)))
|
||||
di7))
|
||||
|
||||
(define (members-base-oldest-month mb)
|
||||
(make-month 2015 1))
|
||||
|
||||
;; Returns a list two lists: keys, data.
|
||||
;; Each data record contains values for all keys.
|
||||
(define (members-base-stats mb)
|
||||
(let ((keys '(month total active suspended students destroyed invalid)))
|
||||
(let mloop ((data '())
|
||||
(month (members-base-oldest-month mb)))
|
||||
(if (month<? month (*current-month*))
|
||||
(let ((bi (parameterize ((*current-month* month))
|
||||
(members-base-info mb))))
|
||||
(let kloop ((row (list (dict-ref bi 'month)))
|
||||
(keys (cdr keys)))
|
||||
(if (null? keys)
|
||||
(mloop (cons (reverse row) data)
|
||||
(month-add month 1))
|
||||
(kloop (cons (length (dict-ref bi (car keys))) row)
|
||||
(cdr keys)))))
|
||||
(list keys (reverse data))))))
|
||||
|
||||
|
||||
;; Returns all free ids
|
||||
(define (get-free-members-ids mb)
|
||||
(let ((ids (list-members-ids mb)))
|
||||
(filter
|
||||
(lambda (id)
|
||||
(not (member id ids)))
|
||||
(gen-all-4digit-primes))))
|
||||
|
||||
;; Generates random vector id.
|
||||
(define (gen-member-id mb)
|
||||
(let* ((fids (get-free-members-ids mb))
|
||||
(vfids (list->vector fids)))
|
||||
(vector-ref vfids (pseudo-random-integer (vector-length vfids)))))
|
||||
|
||||
;; Returns new members base with member records matching the
|
||||
;; predicate processed by proc.
|
||||
(define (members-base-update mb pred? proc)
|
||||
(map (lambda (mr)
|
||||
(if (pred? mr)
|
||||
(proc mr)
|
||||
mr))
|
||||
mb))
|
||||
|
||||
)
|
184
src/members-dir.scm
Normal file
184
src/members-dir.scm
Normal file
|
@ -0,0 +1,184 @@
|
|||
;;
|
||||
;; members-dir.scm
|
||||
;;
|
||||
;; Storage for member files.
|
||||
;;
|
||||
;; ISC License
|
||||
;;
|
||||
;; Copyright 2023 Brmlab, z.s.
|
||||
;; Dominik Pantůček <dominik.pantucek@trustica.cz>
|
||||
;;
|
||||
;; 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 members-dir))
|
||||
|
||||
(module
|
||||
members-dir
|
||||
(
|
||||
load-members-dir
|
||||
members-dir-load-member
|
||||
members-dir-tests!
|
||||
)
|
||||
|
||||
(import scheme
|
||||
(chicken base)
|
||||
(chicken pathname)
|
||||
(chicken file posix)
|
||||
(chicken file)
|
||||
(chicken format)
|
||||
(chicken irregex)
|
||||
testing
|
||||
dictionary
|
||||
utils
|
||||
member-record
|
||||
member-parser)
|
||||
|
||||
;; Gets all files and symbolic links from given directory. The
|
||||
;; symbolic links are represented by cons cells with car being the
|
||||
;; name and cdr the link target.
|
||||
(define (get-files+symlinks dn)
|
||||
(let loop ((fns (directory dn))
|
||||
(rs '()))
|
||||
(if (null? fns)
|
||||
rs
|
||||
(let* ((fn (car fns))
|
||||
(ffn (make-pathname dn fn)))
|
||||
(loop (cdr fns)
|
||||
(if (symbolic-link? ffn)
|
||||
(cons (cons (string->symbol fn)
|
||||
(string->symbol (read-symbolic-link ffn)))
|
||||
rs)
|
||||
(if (regular-file? ffn)
|
||||
(cons (string->symbol fn) rs)
|
||||
rs)))))))
|
||||
|
||||
;; Converts a list of symlinks and files in aforementioned format
|
||||
;; into a dictionary of regular files as keys with lists of symlinks
|
||||
;; as values. If the target file does not exist, adds 'error-0 symbol
|
||||
;; as the first alias to this list with the number increasing with
|
||||
;; each nonexistent file encountered. The error record is also
|
||||
;; generated for symlinks pointing outside of the directory.
|
||||
(define (files+symlinks->files-dictionary ls)
|
||||
(let* ((links (filter pair? ls))
|
||||
(files (filter symbol? ls))
|
||||
(fdict
|
||||
(let loop ((files files)
|
||||
(res (make-dict)))
|
||||
(if (null? files)
|
||||
res
|
||||
(loop (cdr files)
|
||||
(dict-set res (car files) '()))))))
|
||||
(let loop ((links links)
|
||||
(res fdict)
|
||||
(errs 0))
|
||||
(if (null? links)
|
||||
res
|
||||
(let* ((link (car links))
|
||||
(name (car link))
|
||||
(target (cdr link)))
|
||||
(if (dict-has-key? res target)
|
||||
(loop (cdr links)
|
||||
(dict-set res target (cons name (dict-ref res target)))
|
||||
errs)
|
||||
(loop (cdr links)
|
||||
(dict-set res target
|
||||
(list (string->symbol (sprintf "error-~A" errs))
|
||||
name))
|
||||
(+ errs 1))))))))
|
||||
|
||||
|
||||
;; Checks whether given string is a 4-digit decimal number.
|
||||
(define (is-4digit-string? s)
|
||||
(if (irregex-search (irregex "^[0-9]{4}$") s)
|
||||
#t
|
||||
#f))
|
||||
|
||||
;; checks whether given symbol is a 4-digit one.
|
||||
(define (is-4digit-symbol? s)
|
||||
(is-4digit-string?
|
||||
(symbol->string s)))
|
||||
|
||||
;; Returns true if the list contains at least one 4-digit symbol.
|
||||
(define (list-contains-4digit-symbol? lst)
|
||||
(let loop ((lst lst))
|
||||
(if (null? lst)
|
||||
#f
|
||||
(if (is-4digit-symbol? (car lst))
|
||||
#t
|
||||
(loop (cdr lst))))))
|
||||
|
||||
;; Returns the first 4-digit symbol from the list.
|
||||
(define (get-4digit-symbol-from-list lst)
|
||||
(let loop ((lst lst))
|
||||
(if (null? lst)
|
||||
#f
|
||||
(if (is-4digit-symbol? (car lst))
|
||||
(car lst)
|
||||
(loop (cdr lst))))))
|
||||
|
||||
;; Returns dictionary containing only records with either 4-digit
|
||||
;; name or one of its aliases being 4-digit.
|
||||
(define (files-dictionary-filter-4digit-symbols d)
|
||||
(dict-filter
|
||||
(lambda (k v)
|
||||
(list-contains-4digit-symbol? (cons k v)))
|
||||
d))
|
||||
|
||||
;; Loads the members directory as dictionary of files to symlinks
|
||||
;; mapping.
|
||||
(define (load-members-dir dn)
|
||||
(files-dictionary-filter-4digit-symbols
|
||||
(files+symlinks->files-dictionary
|
||||
(get-files+symlinks dn))))
|
||||
|
||||
;; Returns a dictionary containing file-name, symlinks, id and info
|
||||
;; keys. The info key contains whatever load-member-file from the
|
||||
;; member-file module returns. The id key contains whatever is the
|
||||
;; first 4-digit symbol in (cons fname aliases) list.
|
||||
(define (members-dir-load-member mdir fname symlinks)
|
||||
(let* ((mr0 (make-member-record fname
|
||||
(make-pathname mdir (symbol->string fname))
|
||||
symlinks)))
|
||||
(load-member-file mr0)))
|
||||
|
||||
;; Performs self-tests of this module.
|
||||
(define (members-dir-tests!)
|
||||
(run-tests
|
||||
members-base
|
||||
(test-equal? files+symlinks->files-dictionary
|
||||
(files+symlinks->files-dictionary
|
||||
'(joe (2803 . joe)))
|
||||
'((joe 2803)))
|
||||
(test-equal? files+symlinks->files-dictionary
|
||||
(files+symlinks->files-dictionary
|
||||
'(joe
|
||||
(2803 . joe)
|
||||
(666 . nonexistent)))
|
||||
'((nonexistent error-0 666)
|
||||
(joe 2803)))
|
||||
(test-true is-4digit-string? (is-4digit-string? "0000"))
|
||||
(test-false is-4digit-string? (is-4digit-string? "AAAA"))
|
||||
(test-false is-4digit-string? (is-4digit-string? "666"))
|
||||
(test-true is-4digit-symbol? (is-4digit-symbol? '|0000|))
|
||||
(test-false is-4digit-symbol? (is-4digit-symbol? '|ABC|))
|
||||
(test-true list-contains-4digit-symbol? (list-contains-4digit-symbol? '(|0000| abc |666|)))
|
||||
(test-false list-contains-4digit-symbol? (list-contains-4digit-symbol? '(|00000| abc |666|)))
|
||||
(test-eq? get-4digit-symbol-from-list
|
||||
(get-4digit-symbol-from-list '(|000| abc |6666| qwer))
|
||||
'|6666|)
|
||||
))
|
||||
|
||||
)
|
143
src/members-payments.scm
Normal file
143
src/members-payments.scm
Normal file
|
@ -0,0 +1,143 @@
|
|||
;;
|
||||
;; members-payments.scm
|
||||
;;
|
||||
;; Adding payment information to member records from bank account statement.
|
||||
;;
|
||||
;; ISC License
|
||||
;;
|
||||
;; Copyright 2023 Brmlab, z.s.
|
||||
;; Dominik Pantůček <dominik.pantucek@trustica.cz>
|
||||
;;
|
||||
;; 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 members-payments))
|
||||
|
||||
(module
|
||||
members-payments
|
||||
(
|
||||
members-payments-process
|
||||
member-payments-total
|
||||
)
|
||||
|
||||
(import scheme
|
||||
(chicken base)
|
||||
(chicken string)
|
||||
(chicken io)
|
||||
(chicken irregex)
|
||||
(chicken sort)
|
||||
bank-account
|
||||
member-record
|
||||
members-base
|
||||
bank-fio
|
||||
dictionary
|
||||
member-fees
|
||||
period)
|
||||
|
||||
;; Exchange rates
|
||||
(define exchange-rates-lookup-table
|
||||
(make-period-lookup-table
|
||||
'(((2010 1) 25))))
|
||||
|
||||
;; Lookup CZK/EUR
|
||||
(define (lookup-eur-rate)
|
||||
(car (lookup-by-period exchange-rates-lookup-table)))
|
||||
|
||||
;; Extract probable member-id from transaction
|
||||
(define (transaction-extract-member-id transaction)
|
||||
(if (equal? (bank-transaction-type transaction) "Poplatek")
|
||||
#f
|
||||
(let* ((varsym-id0
|
||||
(string->number
|
||||
(bank-transaction-varsym transaction)))
|
||||
(varsym-id
|
||||
(or varsym-id0
|
||||
(let* ((msg (bank-transaction-message transaction))
|
||||
(ci (substring-index "," msg))
|
||||
(vs (if ci
|
||||
(substring msg 0 ci)
|
||||
msg)))
|
||||
(string->number vs)))))
|
||||
varsym-id)))
|
||||
|
||||
;; Merges bank account statement into members payment keys. The
|
||||
;; payment key will be a list of transactions.
|
||||
(define (members-payments-process-bank mb ba)
|
||||
(let loop ((mb mb)
|
||||
(transactions (bank-account-transactions ba)))
|
||||
(if (null? transactions)
|
||||
mb
|
||||
(let* ((transaction (car transactions))
|
||||
(varsym-id (transaction-extract-member-id transaction)))
|
||||
(loop (members-base-update mb
|
||||
(lambda (mr)
|
||||
(eq? (member-id mr)
|
||||
varsym-id))
|
||||
(lambda (mr)
|
||||
(member-record-add-payment mr transaction)))
|
||||
(cdr transactions))))))
|
||||
|
||||
;; Reads the payments
|
||||
(define (load-accounts-list apikeys)
|
||||
(map (compose car string-split)
|
||||
(read-lines
|
||||
(open-input-file apikeys))))
|
||||
|
||||
;; Loads all accounts - it expects .csv files in the current
|
||||
;; directory.
|
||||
(define (load-accounts accounts-list)
|
||||
(map (lambda (acc)
|
||||
(bank-fio-parse (string-append acc ".csv")))
|
||||
accounts-list))
|
||||
|
||||
;; If apikeys is not #f, loads the account numbers, loads bank
|
||||
;; accounts and processes transactions.
|
||||
(define (members-payments-process mb apikeys-file)
|
||||
(if apikeys-file
|
||||
(let* ((accounts (load-accounts
|
||||
(load-accounts-list apikeys-file))))
|
||||
(map member-add-balance
|
||||
(foldl members-payments-process-bank
|
||||
mb
|
||||
accounts)))
|
||||
mb))
|
||||
|
||||
;; Adds all balances - payments are converted to CZK.
|
||||
(define (member-add-balance mr)
|
||||
(let ((mr0 (dict-set mr
|
||||
'balance
|
||||
(make-dict `((fees . ,(member-fees-total mr))
|
||||
(credit . ,(member-credit-total mr))
|
||||
(payment . ,(member-payments-total mr)))))))
|
||||
(dict-set mr0
|
||||
'payments
|
||||
(sort (dict-ref mr0 'payments '())
|
||||
(lambda (a b)
|
||||
(string<? (bank-transaction-date a)
|
||||
(bank-transaction-date b)))))))
|
||||
|
||||
|
||||
;; Total amount paid
|
||||
(define (member-payments-total mr)
|
||||
(foldl + 0 (map (lambda (tr)
|
||||
(let ((amount (bank-transaction-amount tr))
|
||||
(currency (bank-transaction-currency tr)))
|
||||
(case currency
|
||||
((CZK) amount)
|
||||
((EUR) (* amount (lookup-eur-rate)))
|
||||
(else 0))))
|
||||
(member-payments mr))))
|
||||
|
||||
)
|
374
src/members-print.scm
Normal file
374
src/members-print.scm
Normal file
|
@ -0,0 +1,374 @@
|
|||
;;
|
||||
;; members-print.scm
|
||||
;;
|
||||
;; Procedures working with complete member record (as loaded by the
|
||||
;; members-base).
|
||||
;;
|
||||
;; ISC License
|
||||
;;
|
||||
;; Copyright 2023 Brmlab, z.s.
|
||||
;; Dominik Pantůček <dominik.pantucek@trustica.cz>
|
||||
;;
|
||||
;; 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 members-print))
|
||||
|
||||
(module
|
||||
members-print
|
||||
(
|
||||
print-member-info
|
||||
print-member-table
|
||||
print-member-source
|
||||
print-members-base-info
|
||||
print-members-base-table
|
||||
print-members-base-stats
|
||||
print-members-ids-stats
|
||||
print-members-fees-table
|
||||
)
|
||||
|
||||
(import scheme
|
||||
(chicken base)
|
||||
(chicken string)
|
||||
(chicken sort)
|
||||
(chicken format)
|
||||
dictionary
|
||||
member-record
|
||||
month
|
||||
utils
|
||||
table
|
||||
listing
|
||||
ansi
|
||||
period
|
||||
primes
|
||||
members-base
|
||||
configuration
|
||||
bank-account
|
||||
member-fees)
|
||||
|
||||
;; Prints human-readable information
|
||||
(define (print-member-info mr)
|
||||
(let* ((id (dict-ref mr 'id))
|
||||
(aliases (dict-ref mr 'symlinks))
|
||||
(info (dict-ref mr 'info))
|
||||
(sinfo (sort info
|
||||
(lambda (a b)
|
||||
(string<?
|
||||
(symbol->string (car a))
|
||||
(symbol->string (car b)))))))
|
||||
(print "User " id " alias(es): "
|
||||
(string-intersperse
|
||||
(map symbol->string aliases)
|
||||
", "))
|
||||
(when (member-suspended? mr)
|
||||
(print " Suspended for " (member-suspended-months mr) " months."))
|
||||
(newline)
|
||||
(let loop ((sinfo sinfo))
|
||||
(when (not (null? sinfo))
|
||||
(let* ((kv (car sinfo))
|
||||
(k (car kv))
|
||||
(v (cdr kv)))
|
||||
(loop (cdr sinfo)))))))
|
||||
|
||||
;; Returns nicely formatted table
|
||||
(define (member-info->table mr)
|
||||
(let* ((aliases (dict-ref mr 'symlinks))
|
||||
(mid (member-id mr))
|
||||
(head (list (if (is-4digit-prime? mid)
|
||||
(list "ID:" mid)
|
||||
(list (ansi-string #:red #:bold "ID:")
|
||||
(ansi-string #:red #:bold (number->string mid) " (not prime)")))
|
||||
(list (sprintf "Alias~A:" (if (> (length aliases) 1) "es" ""))
|
||||
(string-intersperse (map symbol->string aliases) ", "))
|
||||
(if (member-suspended? mr)
|
||||
(list "Suspended for:"
|
||||
(let ((msm (member-suspended-months mr)))
|
||||
(sprintf "~A month~A" msm
|
||||
(if (> msm 1) "s" ""))))
|
||||
#f)))
|
||||
(info (dict-ref mr 'info))
|
||||
(sikeys (sort (dict-keys info)
|
||||
(lambda (a b)
|
||||
(string<?
|
||||
(symbol->string a)
|
||||
(symbol->string b)))))
|
||||
(body (map (lambda (k)
|
||||
(let ((v (dict-ref info k)))
|
||||
(case k
|
||||
((card desfire credit)
|
||||
(list k
|
||||
(table->string
|
||||
(map
|
||||
(lambda (c)
|
||||
(list (car c) (cdr c)))
|
||||
v)
|
||||
#:col-border #t)))
|
||||
((suspend student member)
|
||||
(list k
|
||||
(table->string
|
||||
(cons (list "Since" "Until")
|
||||
(map
|
||||
(lambda (p)
|
||||
(list
|
||||
(string-append (month->string (period-since p)) " "
|
||||
(or (period-scomment p) ""))
|
||||
(string-append (month->string (period-before p)) " "
|
||||
(or (period-bcomment p) ""))))
|
||||
v))
|
||||
#:col-border #t)))
|
||||
(else
|
||||
(if v
|
||||
(list k v)
|
||||
(list (ansi-string #:red #:bold (symbol->string k))
|
||||
(ansi-string #:red #:bold "---")))))))
|
||||
sikeys))
|
||||
(result (filter identity (append head body))))
|
||||
(table->string result #:table-border #f #:row-border #t #:col-border #t #:ansi #t)))
|
||||
|
||||
;; Prints nicely formatted table
|
||||
(define (print-member-table mr)
|
||||
(print
|
||||
(table->string
|
||||
(list (list "Basic Information" "Payments" "Membership Status")
|
||||
(list (member-info->table mr)
|
||||
(member-payments->table mr)
|
||||
(member-calendar->table mr))
|
||||
)
|
||||
#:row0-border #t
|
||||
#:col-border #t)))
|
||||
|
||||
;; Nicely prints the member source with any errors recorded.
|
||||
(define (print-member-source mr)
|
||||
(let* ((lines (dict-ref mr 'source mr))
|
||||
(file-name (dict-ref mr 'file-name))
|
||||
(hls (dict-ref mr 'highlights '())))
|
||||
(print file-name ":")
|
||||
(print-source-listing
|
||||
lines
|
||||
hls
|
||||
#:context -1
|
||||
)))
|
||||
|
||||
;; Prints nicely printed payments
|
||||
(define (member-payments->table mr)
|
||||
(table->string (cons
|
||||
(list "Var" "Amount" "Cur" "Date" "TrId")
|
||||
(map (lambda (tr)
|
||||
(list (sprintf "\t~A" (bank-transaction-varsym tr))
|
||||
(sprintf "\t~A" (bank-transaction-amount tr))
|
||||
(bank-transaction-currency tr)
|
||||
(bank-transaction-date tr)
|
||||
(bank-transaction-id tr)))
|
||||
(member-payments mr)))
|
||||
#:row0-border #t
|
||||
#:col-border #t
|
||||
))
|
||||
|
||||
;; Converts member records to string, optional arguments are format
|
||||
;; and separator. Format defaults to "~N" and separator to ", ".
|
||||
(define (member-records->string mrs . args)
|
||||
(let ((fmt (if (null? args) "~N" (car args)))
|
||||
(sep (if (or (null? args)
|
||||
(null? (cdr args)))
|
||||
", "
|
||||
(cadr args))))
|
||||
(string-intersperse
|
||||
(map (lambda (mr)
|
||||
(member-format fmt mr))
|
||||
mrs)
|
||||
sep)))
|
||||
|
||||
;; Basic information about members-base in human-readable form.
|
||||
(define (print-members-base-info mb)
|
||||
(let ((nicks (list-members-nicks mb))
|
||||
(ids (list-members-ids mb)))
|
||||
(print "Known members: "
|
||||
(length nicks))
|
||||
(let* ((bi (members-base-info mb))
|
||||
(invalid-mrs (dict-ref bi 'invalid))
|
||||
(active-mrs (dict-ref bi 'active))
|
||||
(suspended-mrs (dict-ref bi 'suspended))
|
||||
(destroyed-mrs (dict-ref bi 'destroyed))
|
||||
(student-mrs (dict-ref bi 'students)))
|
||||
(print a:success " Active (" (length active-mrs) "): " a:default
|
||||
(member-records->string (sort active-mrs member<?) "~N~E"))
|
||||
(print a:warning " Suspended (" (length suspended-mrs) "): " a:default
|
||||
(member-records->string (sort suspended-mrs member<?) "~N~E"))
|
||||
(print a:warning " Destroyed (" (length destroyed-mrs) "): " a:default
|
||||
(member-records->string (sort destroyed-mrs member<?) "~N~E"))
|
||||
(print a:highlight " Students (" (length student-mrs) "): " a:default
|
||||
(member-records->string (sort student-mrs member<?)))
|
||||
(let ((suspended2 (filter-members-by-predicate
|
||||
suspended-mrs
|
||||
(lambda (mr)
|
||||
(>= (member-suspended-months mr) 24)))))
|
||||
(when (not (null? suspended2))
|
||||
(print (ansi #:magenta) " Suspended for at least 24 months ("
|
||||
(length suspended2) "): " a:default
|
||||
(member-records->string (sort suspended2 member<?) "~N (~S)"))))
|
||||
(when (not (null? invalid-mrs))
|
||||
(print a:error " Invalid Id (" (length invalid-mrs) "): "
|
||||
(member-records->string (sort invalid-mrs member<?) "~N (~I)")
|
||||
a:default)))))
|
||||
|
||||
;; Helper function for pretty-formatting the filtered members lists
|
||||
;; in a table.
|
||||
(define (members-table-row a:? label mrs fmt)
|
||||
(list (string-append "\t" a:? label)
|
||||
(length mrs)
|
||||
(ansi-paragraph-format
|
||||
(member-records->string
|
||||
(sort mrs member<?)
|
||||
fmt)
|
||||
60)))
|
||||
|
||||
;; Prints nicely aligned members base info
|
||||
(define (print-members-base-table mb)
|
||||
(let* ((bi (members-base-info mb))
|
||||
(all-mrs (dict-ref bi 'total))
|
||||
(invalid-mrs (dict-ref bi 'invalid))
|
||||
(active-mrs (dict-ref bi 'active))
|
||||
(suspended-mrs (dict-ref bi 'suspended))
|
||||
(destroyed-mrs (dict-ref bi 'destroyed))
|
||||
(student-mrs (dict-ref bi 'students)))
|
||||
(print "Known members: " (length all-mrs))
|
||||
(newline)
|
||||
(print
|
||||
(table->string
|
||||
(filter
|
||||
identity
|
||||
(list (list "Type" "Count" "List")
|
||||
(members-table-row a:success "Active:" active-mrs "~N~E")
|
||||
(members-table-row a:highlight "Students:" student-mrs "~N~E")
|
||||
(members-table-row a:warning "Suspended:" suspended-mrs "~N~E")
|
||||
(members-table-row a:warning "Destroyed:" destroyed-mrs "~N~E")
|
||||
(let ((suspended2 (filter-members-by-predicate
|
||||
suspended-mrs
|
||||
(lambda (mr)
|
||||
(>= (member-suspended-months mr)
|
||||
(*member-suspend-max-months*))))))
|
||||
(if (null? suspended2)
|
||||
#f
|
||||
(members-table-row (ansi #:magenta) "Suspended (long):" suspended2 "~N (~S)")))
|
||||
))
|
||||
#:ansi #t
|
||||
#:row-border #t
|
||||
#:col-border #t
|
||||
)))
|
||||
(let ((pmrs (filter-members-by-predicate mb member-has-problems?)))
|
||||
(when (not (null? pmrs))
|
||||
(newline)
|
||||
(print "Member files with errors (" (length pmrs) "): "
|
||||
(string-intersperse
|
||||
(map member-file-path pmrs)
|
||||
", "))))
|
||||
(let ((pmrs (filter-members-by-predicate mb (lambda (mr)
|
||||
(and (member-has-highlights? mr)
|
||||
(not (member-has-problems? mr)))))))
|
||||
(when (not (null? pmrs))
|
||||
(newline)
|
||||
(print "Member files with issues: "
|
||||
(string-intersperse
|
||||
(map member-file-path pmrs)
|
||||
", ")))))
|
||||
|
||||
;; Prints the stats in format used by gnuplot.
|
||||
(define (print-members-base-stats ms)
|
||||
(let ((keys (car ms))
|
||||
(data (cadr ms)))
|
||||
(print "# " (string-intersperse (map symbol->string keys) " "))
|
||||
(let loop ((rows data))
|
||||
(when (not (null? rows))
|
||||
(let* ((row (car rows))
|
||||
(month (month->string (car row)))
|
||||
(vals (cdr row)))
|
||||
(print month " " (string-intersperse (map number->string vals) " "))
|
||||
(loop (cdr rows)))))))
|
||||
|
||||
;; Prints statistics about allocated and unused valid/invalid IDs.
|
||||
(define (print-members-ids-stats MB)
|
||||
(print "Allocated IDs: "
|
||||
(length (list-members-ids MB))
|
||||
"/"
|
||||
(length (gen-all-4digit-primes))
|
||||
" ("
|
||||
(length (get-free-members-ids MB))
|
||||
" free)")
|
||||
(let ((iids (filter (compose not is-4digit-prime?) (list-members-ids MB))))
|
||||
(when (not (null? iids))
|
||||
(print " Invalid: "
|
||||
(length iids)
|
||||
" ("
|
||||
(string-intersperse
|
||||
(map (lambda (id)
|
||||
(let ((mr (find-member-by-id MB id)))
|
||||
(member-format
|
||||
"~I - ~N"
|
||||
mr)))
|
||||
iids)
|
||||
", ")
|
||||
")"))))
|
||||
|
||||
;; Prints summary table of all fees and credits for all members
|
||||
(define (print-members-fees-table MB)
|
||||
(print
|
||||
(table->string
|
||||
(cons
|
||||
(list (ansi-string #:bgblue #:brightyellow #:bold "Member")
|
||||
(ansi-string #:bgblue #:brightyellow #:bold "Fees")
|
||||
(ansi-string #:bgblue #:brightyellow #:bold "Credit")
|
||||
(ansi-string #:bgblue #:brightyellow #:bold "Payments")
|
||||
(ansi-string #:bgblue #:brightyellow #:bold "Balance"))
|
||||
(append
|
||||
(map
|
||||
(lambda (mr)
|
||||
(let* ((balance (member-balance mr))
|
||||
(fees (dict-ref balance 'fees))
|
||||
(credit (dict-ref balance 'credit))
|
||||
(payment (dict-ref balance 'payment))
|
||||
(total (- (+ credit payment) fees)))
|
||||
(list (member-nick mr)
|
||||
(sprintf "\t~A" fees)
|
||||
(sprintf "\t~A" credit)
|
||||
(sprintf "\t~A" payment)
|
||||
(sprintf "\t~A~A~A"
|
||||
(if (< total -500)
|
||||
a:error
|
||||
(if (< total 0)
|
||||
a:warning
|
||||
a:success))
|
||||
(exact->inexact total)
|
||||
a:default)
|
||||
)))
|
||||
(sort (filter-members-by-predicate MB member-active?) member<?))
|
||||
(let* ((balances (map member-balance MB))
|
||||
(fees (foldl + 0 (map (lambda (b) (dict-ref b 'fees)) balances)))
|
||||
(credit (foldl + 0 (map (lambda (b) (dict-ref b 'credit)) balances)))
|
||||
(payment (foldl + 0 (map (lambda (b) (dict-ref b 'payment)) balances)))
|
||||
(total (- (+ credit payment) fees)))
|
||||
(list (list (ansi-string #:bold "Total")
|
||||
(ansi-string "\t" #:bold (sprintf "~A" fees))
|
||||
(ansi-string "\t" #:bold (sprintf "~A" credit))
|
||||
(ansi-string "\t" #:bold (sprintf "~A" payment))
|
||||
(ansi-string "\t" #:bold
|
||||
(sprintf "~A~A"
|
||||
(if (< total 0)
|
||||
a:error
|
||||
a:success)
|
||||
total))
|
||||
)))))
|
||||
#:col-border #t #:row0-border #t #:ansi #t)))
|
||||
|
||||
)
|
182
src/month.scm
Normal file
182
src/month.scm
Normal file
|
@ -0,0 +1,182 @@
|
|||
;;
|
||||
;; month.scm
|
||||
;;
|
||||
;; Month processing support.
|
||||
;;
|
||||
;; ISC License
|
||||
;;
|
||||
;; Copyright 2023 Brmlab, z.s.
|
||||
;; Dominik Pantůček <dominik.pantucek@trustica.cz>
|
||||
;;
|
||||
;; 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>=?
|
||||
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)
|
||||
(and (month-valid? m)
|
||||
(month-valid? n)
|
||||
(or (< (car m) (car n))
|
||||
(and (= (car m) (car n))
|
||||
(< (cadr m) (cadr n))))))
|
||||
|
||||
;; Returns true if m is less than or equal n
|
||||
(define (month<=? m n)
|
||||
(or (month<? m n)
|
||||
(month=? m n)))
|
||||
|
||||
;; Returns true if m is greater than or equal to n
|
||||
(define (month>=? m n)
|
||||
(not (month<? m n)))
|
||||
|
||||
;; Returns true if m is greater than n
|
||||
(define (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<? (month<? '(2023 5) '(2023 6)))
|
||||
(test-true month<? (month<? '(2022 12) '(2023 1)))
|
||||
(test-false month<? (month<? '(2023 1) '(2023 1)))
|
||||
(test-false month<? (month<? '(2023 1) '(2023 1)))
|
||||
(test-true month=? (month=? '(2023 4) '(2023 4)))
|
||||
(test-false month=? (month=? '(2023 4) '(2023 5)))
|
||||
(test-eq? month-diff (month-diff '(2023 1) '(2023 2)) 1)
|
||||
(test-eq? month-diff (month-diff '(2023 1) '(2023 12)) 11)
|
||||
(test-eq? month-diff (month-diff '(2023 1) '(2022 2)) -11)
|
||||
(test-eq? month-add (month-add '(2023 1) 2) '(2023 3))
|
||||
))
|
||||
|
||||
)
|
268
src/period.scm
Normal file
268
src/period.scm
Normal file
|
@ -0,0 +1,268 @@
|
|||
;;
|
||||
;; period.scm
|
||||
;;
|
||||
;; Month periods.
|
||||
;;
|
||||
;; ISC License
|
||||
;;
|
||||
;; Copyright 2023 Brmlab, z.s.
|
||||
;; Dominik Pantůček <dominik.pantucek@trustica.cz>
|
||||
;;
|
||||
;; 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 period))
|
||||
|
||||
(module
|
||||
period
|
||||
(
|
||||
make-period
|
||||
period-since
|
||||
period-before
|
||||
period-scomment
|
||||
period-bcomment
|
||||
period-markers->periods
|
||||
periods-duration
|
||||
month-in-period?
|
||||
month-in-periods?
|
||||
periods->string
|
||||
periods-match
|
||||
make-period-lookup-table
|
||||
lookup-by-period
|
||||
period-tests!
|
||||
)
|
||||
|
||||
(import scheme
|
||||
(chicken base)
|
||||
(chicken sort)
|
||||
(chicken time)
|
||||
(chicken time posix)
|
||||
(chicken format)
|
||||
(chicken string)
|
||||
month
|
||||
testing
|
||||
configuration)
|
||||
|
||||
;; Creates a new period value with optional since and before
|
||||
;; comments.
|
||||
(define (make-period since before . args)
|
||||
(let ((scomment (if (not (null? args)) (car args) #f))
|
||||
(bcomment (if (and (not (null? args))
|
||||
(not (null? (cdr args))))
|
||||
(cadr args)
|
||||
#f)))
|
||||
(list since before scomment bcomment)))
|
||||
|
||||
;; Simple accessors
|
||||
(define period-since car)
|
||||
(define period-before cadr)
|
||||
(define period-scomment caddr)
|
||||
(define period-bcomment cadddr)
|
||||
|
||||
;; Sorts period markers (be it start or end) chronologically and
|
||||
;; returns the sorted list.
|
||||
(define (sort-period-markers l)
|
||||
(sort l
|
||||
(lambda (a b)
|
||||
(month<? (cadr a) (cadr b)))))
|
||||
|
||||
;; Converts list of start/stop markers to list of pairs of months -
|
||||
;; periods.
|
||||
(define (period-markers->periods l)
|
||||
(let loop ((l (sort-period-markers l))
|
||||
(ps '())
|
||||
(cb #f))
|
||||
(if (null? l)
|
||||
(list #t
|
||||
(if cb
|
||||
(reverse (cons (make-period (car cb) #f (cadr cb)) ps))
|
||||
(reverse ps))
|
||||
""
|
||||
-1)
|
||||
(let* ((marker (car l))
|
||||
(rmt (if cb 'stop 'start))
|
||||
(mtype (car marker))
|
||||
(month (cadr marker))
|
||||
(line-number (if (null? (cddr marker))
|
||||
#f
|
||||
(caddr marker)))
|
||||
(comment (if (and line-number
|
||||
(not (null? (cdddr marker))))
|
||||
(cadddr marker)
|
||||
#f)))
|
||||
(if (eq? mtype rmt)
|
||||
(if cb
|
||||
(loop (cdr l)
|
||||
(cons (make-period (car cb) month (cadr cb) comment) ps)
|
||||
#f)
|
||||
(loop (cdr l)
|
||||
ps
|
||||
(list month comment)))
|
||||
(list #f
|
||||
(reverse ps)
|
||||
(sprintf "Invalid start/stop sequence marker ~A" marker)
|
||||
line-number))))))
|
||||
|
||||
;; Returns duration of period in months. Start is included, end is
|
||||
;; not. The period contains the month just before the specified end.
|
||||
(define (period->duration p)
|
||||
(let* ((b (period-since p))
|
||||
(e (period-before p))
|
||||
(e- (if e e (*current-month*))))
|
||||
(month-diff b e-)))
|
||||
|
||||
;; Returns sum of periods lengths.
|
||||
(define (periods-duration l)
|
||||
(apply + (map period->duration l)))
|
||||
|
||||
;; True if month belongs to given month period - start inclusive, end
|
||||
;; exclusive.
|
||||
(define (month-in-period? p . ml)
|
||||
(let ((m (if (null? ml)
|
||||
(*current-month*)
|
||||
(car ml))))
|
||||
(and (or (not (period-before p))
|
||||
(month<? m (period-before p)))
|
||||
(not (month<? m (period-since p))))))
|
||||
|
||||
;; Returns true if given month is in at least one of the periods
|
||||
;; given. Defaults to current month.
|
||||
(define (month-in-periods? ps . ml)
|
||||
(let ((m (if (null? ml)
|
||||
(*current-month*)
|
||||
(car ml))))
|
||||
(let loop ((ps ps))
|
||||
(if (null? ps)
|
||||
#f
|
||||
(if (month-in-period? (car ps) m)
|
||||
#t
|
||||
(loop (cdr ps)))))))
|
||||
|
||||
;; Returns string representing a month period with possibly open end.
|
||||
(define (period->string p)
|
||||
(sprintf "~A..~A"
|
||||
(month->string (period-since p))
|
||||
(month->string (period-before p))))
|
||||
|
||||
;; Returns a string representing a list of periods.
|
||||
(define (periods->string ps)
|
||||
(string-intersperse
|
||||
(map period->string ps)
|
||||
", "))
|
||||
|
||||
;; Finds a period the month matches and returns it. If no period
|
||||
;; matches, it returns #f.
|
||||
(define (periods-match ps . ml)
|
||||
(let ((m (if (null? ml) (*current-month*) (car ml))))
|
||||
(let loop ((ps ps))
|
||||
(if (null? ps)
|
||||
#f
|
||||
(if (month-in-period? (car ps) m)
|
||||
(car ps)
|
||||
(loop (cdr ps)))))))
|
||||
|
||||
;; Creates lookup table from definition source
|
||||
(define (make-period-lookup-table source)
|
||||
(let loop ((lst source)
|
||||
(res '())
|
||||
(prev #f))
|
||||
(if (null? lst)
|
||||
(reverse
|
||||
(cons (cons (make-period (car prev) #f)
|
||||
(cdr prev))
|
||||
res))
|
||||
(loop (cdr lst)
|
||||
(if prev
|
||||
(cons (cons (make-period (car prev) (caar lst))
|
||||
(cdr prev))
|
||||
res)
|
||||
res)
|
||||
(car lst)))))
|
||||
|
||||
;; Looks up current month and returns associated definitions
|
||||
(define (lookup-by-period table)
|
||||
(let loop ((lst table))
|
||||
(if (null? lst)
|
||||
#f
|
||||
(if (month-in-period? (caar lst))
|
||||
(cdar lst)
|
||||
(loop (cdr lst))))))
|
||||
|
||||
;; Performs self-tests of the period module.
|
||||
(define (period-tests!)
|
||||
(run-tests
|
||||
period
|
||||
(test-equal? sort-period-markers
|
||||
(sort-period-markers '((start (2023 1)) (stop (2022 10)) (start (2022 3))))
|
||||
'((start (2022 3)) (stop (2022 10)) (start (2023 1))))
|
||||
(test-equal? period-markers->periods
|
||||
(period-markers->periods
|
||||
'((start (2022 3)) (stop (2022 10)) (start (2023 1)) (stop (2023 4))))
|
||||
'(#t
|
||||
(((2022 3) (2022 10) #f #f)
|
||||
((2023 1) (2023 4) #f #f))
|
||||
""
|
||||
-1))
|
||||
(test-equal? period-markers->periods-open
|
||||
(period-markers->periods
|
||||
'((start (2022 3)) (stop (2022 10)) (start (2023 1)) (stop (2023 4)) (start (2023 5))))
|
||||
'(#t
|
||||
(((2022 3) (2022 10) #f #f)
|
||||
((2023 1) (2023 4) #f #f)
|
||||
((2023 5) #f #f #f))
|
||||
""
|
||||
-1))
|
||||
(test-eq? period-duration
|
||||
(period->duration '((2023 1) (2023 4) #f #f)) 3)
|
||||
(parameterize ((*current-month* (list 2023 4)))
|
||||
(test-eq? period-duration
|
||||
(period->duration '((2023 1) #f #f #f)) 3))
|
||||
(test-eq? periods-duration
|
||||
(periods-duration '(((2022 3) (2022 10) #f #f)
|
||||
((2023 1) (2023 4) #f #f)))
|
||||
10)
|
||||
(test-true month-in-period?
|
||||
(month-in-period? '((2022 1) (2022 4) #f #f) '(2022 3)))
|
||||
(test-false month-in-period?
|
||||
(month-in-period? '((2022 1) (2022 4) #f #f) '(2022 5)))
|
||||
(test-true month-in-periods?
|
||||
(month-in-periods? '(((2022 1) (2022 4) #f #f)
|
||||
((2023 5) (2023 10) #f #f))
|
||||
'(2022 3)))
|
||||
(test-true month-in-periods?
|
||||
(month-in-periods? '(((2022 1) (2022 4) #f #f)
|
||||
((2023 5) (2023 10) #f #f))
|
||||
'(2023 7)))
|
||||
(test-false month-in-periods?
|
||||
(month-in-periods? '(((2022 1) (2022 4) #f #f)
|
||||
((2023 5) (2023 10) #f #f))
|
||||
'(2022 10)))
|
||||
(test-equal? period->string
|
||||
(period->string '((2022 1) (2022 4) #f #f))
|
||||
"2022-01..2022-04")
|
||||
(test-equal? periods->string
|
||||
(periods->string '(((2022 1) (2022 4) #f #f)
|
||||
((2022 12) (2023 2) #f #f)))
|
||||
"2022-01..2022-04, 2022-12..2023-02")
|
||||
(test-false periods-match (periods-match '(((2022 1) (2022 4) #f #f)
|
||||
((2022 12) (2023 2) #f #f))
|
||||
'(2022 5)))
|
||||
(test-equal? periods-match (periods-match '(((2022 1) (2022 4) #f #f)
|
||||
((2022 12) (2023 2) #f #f))
|
||||
'(2022 2))
|
||||
'((2022 1) (2022 4) #f #f))
|
||||
))
|
||||
|
||||
)
|
100
src/primes.scm
Normal file
100
src/primes.scm
Normal file
|
@ -0,0 +1,100 @@
|
|||
;;
|
||||
;; primes.scm
|
||||
;;
|
||||
;; Simple handling of 4-digit primes.
|
||||
;;
|
||||
;; ISC License
|
||||
;;
|
||||
;; Copyright 2023 Brmlab, z.s.
|
||||
;; Dominik Pantůček <dominik.pantucek@trustica.cz>
|
||||
;;
|
||||
;; 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 primes))
|
||||
|
||||
(module
|
||||
primes
|
||||
(
|
||||
is-4digit-prime?
|
||||
gen-all-4digit-primes
|
||||
primes-tests!
|
||||
)
|
||||
|
||||
(import scheme
|
||||
(chicken base)
|
||||
testing
|
||||
utils)
|
||||
|
||||
;; Checks whether given number is prime by checking the remainder of
|
||||
;; the division by all primes less than square root of the number in
|
||||
;; question.
|
||||
(define (check-prime primes n)
|
||||
(let ((prime-at-most
|
||||
(inexact->exact
|
||||
(floor
|
||||
(sqrt n)))))
|
||||
(let loop ((ps primes))
|
||||
(if (null? ps)
|
||||
#t
|
||||
(let ((cp (car ps)))
|
||||
(if (<= cp prime-at-most)
|
||||
(if (eq? (remainder n cp) 0)
|
||||
#f
|
||||
(loop (cdr ps)))
|
||||
#t))))))
|
||||
|
||||
;; Generates list of primes less than given argument.
|
||||
(define (gen-primes less-than . init)
|
||||
(let loop ((primes (if (null? init)
|
||||
'(2)
|
||||
(car init)))
|
||||
(number 3))
|
||||
(if (< number less-than)
|
||||
(loop (if (check-prime primes number)
|
||||
(append primes (list number))
|
||||
primes)
|
||||
(+ number 2))
|
||||
primes)))
|
||||
|
||||
;; Used for checking any primes < 10000
|
||||
(define primes<100 (gen-primes 100))
|
||||
|
||||
;; Check whether given number is four-digit number and whether it is
|
||||
;; also prime.
|
||||
(define (is-4digit-prime? n)
|
||||
(and (>= n 1000)
|
||||
(<= n 9999)
|
||||
(check-prime primes<100 n)))
|
||||
|
||||
;; Generates all valid member ids
|
||||
(define (gen-all-4digit-primes)
|
||||
(filter is-4digit-prime?
|
||||
(gen-primes 10000 primes<100)))
|
||||
|
||||
;; Module self-tests.
|
||||
(define (primes-tests!)
|
||||
(run-tests
|
||||
primes
|
||||
(test-true check-prime (check-prime primes<100 67))
|
||||
(test-true is-4digit-prime? (is-4digit-prime? 2803))
|
||||
(test-false is-4digit-prime? (is-4digit-prime? 666))
|
||||
(test-false is-4digit-prime? (is-4digit-prime? 997))
|
||||
(test-false is-4digit-prime? (is-4digit-prime? 6666))
|
||||
(test-false is-4digit-prime? (is-4digit-prime? 66666))
|
||||
(test-false is-4digit-prime? (is-4digit-prime? 10007))
|
||||
))
|
||||
|
||||
)
|
166
src/progress.scm
Normal file
166
src/progress.scm
Normal file
|
@ -0,0 +1,166 @@
|
|||
;;
|
||||
;; progress.scm
|
||||
;;
|
||||
;; Progress reporting.
|
||||
;;
|
||||
;; ISC License
|
||||
;;
|
||||
;; Copyright 2023 Brmlab, z.s.
|
||||
;; Dominik Pantůček <dominik.pantucek@trustica.cz>
|
||||
;;
|
||||
;; 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 progress))
|
||||
|
||||
(module
|
||||
progress
|
||||
(
|
||||
run-with-progress
|
||||
progress-advance
|
||||
with-progress
|
||||
run-progress-break
|
||||
progress-break
|
||||
|
||||
*progress%-width*
|
||||
*progress%-step*
|
||||
run-with-progress%
|
||||
with-progress%
|
||||
progress%-advance
|
||||
)
|
||||
|
||||
(import scheme
|
||||
(chicken base)
|
||||
(chicken format)
|
||||
(chicken port))
|
||||
|
||||
;; Parameterized current progress string
|
||||
(define *current-progress* (make-parameter #f))
|
||||
|
||||
;; Prints current progress.
|
||||
(define (print-current-progress . args)
|
||||
(let ((cp (*current-progress*)))
|
||||
(when cp
|
||||
(display (sprintf "\r\x1b[K~A" cp)))))
|
||||
|
||||
;; Adds something to current progress and refreshes the display.
|
||||
(define (progress-advance . args)
|
||||
(when (*current-progress*)
|
||||
(let ((str (if (null? args)
|
||||
"."
|
||||
(car args))))
|
||||
(*current-progress* (string-append (*current-progress*) (sprintf "~A" str)))
|
||||
(print-current-progress))))
|
||||
|
||||
;; Runs given procedure within progress environment
|
||||
(define (run-with-progress echo? pre-msg post-msg thunk)
|
||||
(parameterize ((*current-progress* (if echo? pre-msg #f)))
|
||||
(print-current-progress)
|
||||
(let ((result (thunk)))
|
||||
(print-current-progress)
|
||||
(when echo?
|
||||
(print post-msg))
|
||||
result)))
|
||||
|
||||
;; Allows printing output when progress is advancing.
|
||||
(define (run-progress-break thunk)
|
||||
(when (*current-progress*)
|
||||
(display "\r\x1b[K"))
|
||||
(thunk)
|
||||
(print-current-progress))
|
||||
|
||||
;; Friendly syntax wrapper.
|
||||
(define-syntax with-progress
|
||||
(syntax-rules ()
|
||||
((_ echo? pre post body ...)
|
||||
(run-with-progress echo? pre post (lambda () body ...)))))
|
||||
|
||||
;; Evaluate some expressions without progress.
|
||||
(define-syntax progress-break
|
||||
(syntax-rules ()
|
||||
((_ body ...)
|
||||
(run-progress-break (lambda () body ...)))))
|
||||
|
||||
;; Progress% visual configuration
|
||||
(define *progress%-width* (make-parameter 40))
|
||||
|
||||
;; Progress% time step configuration
|
||||
(define *progress%-step* (make-parameter 0.01))
|
||||
|
||||
;; Tracking the progress via set of parameters
|
||||
(define *current-progress%* (make-parameter #f))
|
||||
(define *current-progress%-echo?* (make-parameter #f))
|
||||
(define *current-progress%-value* (make-parameter #f))
|
||||
(define *current-progress%-last-value* (make-parameter #f))
|
||||
(define *current-progress%-range* (make-parameter (cons 0 1)))
|
||||
|
||||
;; Unconditionally prints the current progress.
|
||||
(define (print-current-progress%)
|
||||
(when (and (*current-progress%*)
|
||||
(*current-progress%-echo?*))
|
||||
(let* ((raw-value (*current-progress%-value*))
|
||||
(range (*current-progress%-range*))
|
||||
(value (+ (car range)
|
||||
(* raw-value
|
||||
(- (cdr range) (car range)))))
|
||||
(value% (* 100 value))
|
||||
(ivalue% (inexact->exact (round value%)))
|
||||
(bwidth (inexact->exact
|
||||
(round
|
||||
(* value (*progress%-width*)))))
|
||||
(swidth (- (*progress%-width*) bwidth)))
|
||||
(display
|
||||
(sprintf "\r[~A~A] ~A% ~A"
|
||||
(make-string bwidth #\=)
|
||||
(make-string swidth #\space)
|
||||
ivalue%
|
||||
(*current-progress%*))))))
|
||||
|
||||
;; If the new value is different-enough from the current one, updates
|
||||
;; it and re-prints the progress%
|
||||
(define (progress%-advance new-value)
|
||||
(when (*current-progress%*)
|
||||
(*current-progress%-value* new-value)
|
||||
(let ((old-value (*current-progress%-last-value*)))
|
||||
(when (>= (abs (- new-value old-value)) (*progress%-step*))
|
||||
(*current-progress%-last-value* new-value)
|
||||
(print-current-progress%)))))
|
||||
|
||||
;; Procedure realizing the actual progress tracking
|
||||
(define (run-with-progress% echo? name thunk)
|
||||
(parameterize ((*current-progress%* name)
|
||||
(*current-progress%-echo?* echo?)
|
||||
(*current-progress%-value* 0)
|
||||
(*current-progress%-last-value* 0)
|
||||
(*current-progress%-range* (cons 0 1)))
|
||||
(print-current-progress%)
|
||||
(let ((result (thunk)))
|
||||
(print-current-progress%)
|
||||
(when echo?
|
||||
(newline))
|
||||
result)))
|
||||
|
||||
;; Runs named progress%
|
||||
(define-syntax with-progress%
|
||||
(syntax-rules ()
|
||||
((_ echo? name body ...)
|
||||
(run-with-progress%
|
||||
echo? name
|
||||
(lambda () body ...)))))
|
||||
|
||||
;; If the program uses progress module, disable buffering
|
||||
(set-buffering-mode! (current-output-port) #:none)
|
||||
|
||||
)
|
394
src/table.scm
Normal file
394
src/table.scm
Normal file
|
@ -0,0 +1,394 @@
|
|||
;;
|
||||
;; table.scm
|
||||
;;
|
||||
;; Simple table formatter.
|
||||
;;
|
||||
;; ISC License
|
||||
;;
|
||||
;; Copyright 2023 Brmlab, z.s.
|
||||
;; Dominik Pantůček <dominik.pantucek@trustica.cz>
|
||||
;;
|
||||
;; 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 table))
|
||||
|
||||
(module
|
||||
table
|
||||
(
|
||||
*table-border-style*
|
||||
table->string
|
||||
table-tests!
|
||||
)
|
||||
|
||||
(import scheme
|
||||
(chicken base)
|
||||
(chicken string)
|
||||
(chicken format)
|
||||
(chicken keyword)
|
||||
(chicken irregex)
|
||||
ansi
|
||||
testing
|
||||
utils)
|
||||
|
||||
;; Default table border style to use if not explicitly specified.
|
||||
(define *table-border-style* (make-parameter 'unicode))
|
||||
|
||||
;; Table border styles in visual form
|
||||
(define table-borders-lookup-source
|
||||
'((debug
|
||||
"/=,\\"
|
||||
"] |["
|
||||
">-+<"
|
||||
"'~^`")
|
||||
(ascii
|
||||
"+-++"
|
||||
"| ||"
|
||||
"+-++"
|
||||
"+-++")
|
||||
(unicode
|
||||
"┌─┬┐"
|
||||
"│ ││"
|
||||
"├─┼┤"
|
||||
"└─┴┘")))
|
||||
|
||||
;; Returns a list of strings representing the rows in the original
|
||||
;; string.
|
||||
(define (string->rows str)
|
||||
(string-split str "\n" #t))
|
||||
|
||||
;; Creates procedure that ensures a list has given number of elements
|
||||
;; filling the missing elements with given filler (defaults to empty
|
||||
;; string).
|
||||
(define ((make-list-extender ds . ofl) row)
|
||||
(let ((fl (if (null? ofl) "" (car ofl))))
|
||||
(let ((rs (length row)))
|
||||
(if (< rs ds)
|
||||
(let loop ((rrow (reverse row))
|
||||
(rs rs))
|
||||
(if (eq? rs ds)
|
||||
(reverse rrow)
|
||||
(loop (cons fl rrow)
|
||||
(add1 rs))))
|
||||
row))))
|
||||
|
||||
;; Accepts list of lists and makes sure all rows contain the same
|
||||
;; number of elements using empty strings as filler.
|
||||
(define (table-rectangularize tbl)
|
||||
(let ((mrl (apply max (map length tbl))))
|
||||
(map (make-list-extender mrl) tbl)))
|
||||
|
||||
;; Accepts list of lists of anything and returns a list of lists of
|
||||
;; strings.
|
||||
(define (table-stringify tbl)
|
||||
(map
|
||||
(lambda (r)
|
||||
(map (lambda (c) (sprintf "~A" c)) r))
|
||||
tbl))
|
||||
|
||||
;; Converts a 2D table - list of list of strings - into a table of
|
||||
;; cell lists with cell text lines.
|
||||
(define (table-prepare-cells tbl)
|
||||
(map
|
||||
(lambda (r)
|
||||
(map string->rows r))
|
||||
tbl))
|
||||
|
||||
;; Accepts a list of cells which are list of strings and returns a
|
||||
;; new list with all cells having the same number of text lines.
|
||||
(define (table-normalize-row row)
|
||||
(let ((ml (apply max (map length row))))
|
||||
(map (make-list-extender ml) row)))
|
||||
|
||||
;; Normalizes the number of text lines in each table row.
|
||||
(define (table-normalize-rows tbl)
|
||||
(map table-normalize-row tbl))
|
||||
|
||||
;; Returns the maximum width of each column of the table.
|
||||
(define (table-column-widths tbl)
|
||||
(if (null? tbl)
|
||||
'()
|
||||
(let ((cws (map
|
||||
(lambda (r)
|
||||
(list->vector
|
||||
(map
|
||||
(lambda (c)
|
||||
(apply max (map ansi-string-length c)))
|
||||
r)))
|
||||
tbl)))
|
||||
(let loop ((ci (sub1 (vector-length (car cws))))
|
||||
(rcws '()))
|
||||
(if (>= ci 0)
|
||||
(loop (sub1 ci)
|
||||
(cons (apply max (map (lambda (r) (vector-ref r ci)) cws))
|
||||
rcws))
|
||||
rcws)))))
|
||||
|
||||
;; Normalizes cell line to required width and handles leading and
|
||||
;; trailing tabs to allow for right and center alignment.
|
||||
(define (table-normalize-cell-line line w)
|
||||
(let* ((lst (string->list line))
|
||||
(first-char (if (null? lst) #f (car lst)))
|
||||
(last-char (if (or (null? lst)
|
||||
(null? (cdr lst)))
|
||||
#f (car (reverse lst))))
|
||||
(first-tab (eq? first-char #\tab))
|
||||
(last-tab (eq? last-char #\tab))
|
||||
(line0 (if first-tab (substring line 1) line))
|
||||
(line1 (if last-tab (substring line0 0 (sub1 (string-length line0))) line0))
|
||||
(len (ansi-string-length line1)))
|
||||
(if (< len w)
|
||||
(let* ((miss (- w len))
|
||||
(do-left-pad first-tab)
|
||||
(do-right-pad (or (not first-tab) last-tab))
|
||||
(left-pad-len (if do-left-pad
|
||||
(if do-right-pad
|
||||
(- miss (quotient miss 2))
|
||||
miss)
|
||||
0))
|
||||
(right-pad-len (- miss left-pad-len))
|
||||
(left-pad (make-string left-pad-len #\space))
|
||||
(right-pad (make-string right-pad-len #\space)))
|
||||
(string-append left-pad line1 right-pad))
|
||||
line1)))
|
||||
|
||||
;; Pads all lines of this cell to required width
|
||||
(define (table-normalize-cell c w)
|
||||
(map (lambda (line)
|
||||
(table-normalize-cell-line line w))
|
||||
c))
|
||||
|
||||
;; Returns a row (list) of cells (list of strings) with all strings
|
||||
;; padded to given column width.
|
||||
(define (table-row-normalize-cells row cwidths)
|
||||
(let loop ((cwidths cwidths)
|
||||
(cells row)
|
||||
(res '()))
|
||||
(if (null? cells)
|
||||
(reverse res)
|
||||
(loop (cdr cwidths)
|
||||
(cdr cells)
|
||||
(cons (table-normalize-cell (car cells) (car cwidths))
|
||||
res)))))
|
||||
|
||||
;; Normalizes cells in all rows to match the widths of the wides cell
|
||||
;; in each column.
|
||||
(define (table-normalize-columns tbl)
|
||||
(let ((cwidths (table-column-widths tbl)))
|
||||
(map (lambda (row)
|
||||
(table-row-normalize-cells row cwidths))
|
||||
tbl)))
|
||||
|
||||
;; Ensures the table is rectangular and each cell is a list of strings.
|
||||
(define (table-prepare tbl)
|
||||
(table-normalize-columns
|
||||
(table-normalize-rows
|
||||
(table-prepare-cells
|
||||
(table-stringify
|
||||
(table-rectangularize tbl))))))
|
||||
|
||||
;; Compiled table borders for rendering
|
||||
(define table-borders-lookup
|
||||
(map (lambda (src)
|
||||
(cons (car src)
|
||||
(list->vector
|
||||
(irregex-extract (irregex "." 'u)
|
||||
(string-intersperse (cdr src) "")))))
|
||||
table-borders-lookup-source))
|
||||
|
||||
;; Accepts a table row - list of list of strings - and returns a list
|
||||
;; of lines (list of strings).
|
||||
(define (table-row->lines row left-border cell0-separator cell-separator right-border ansi?)
|
||||
(if (null? row)
|
||||
'()
|
||||
(let yloop ((row row)
|
||||
(res '()))
|
||||
(if (null? (car row))
|
||||
(reverse res)
|
||||
(yloop (map cdr row)
|
||||
(cons
|
||||
(string-append
|
||||
left-border
|
||||
(let cloop ((srow (map car row))
|
||||
(res "")
|
||||
(idx 0))
|
||||
(if (null? srow)
|
||||
res
|
||||
(cloop (cdr srow)
|
||||
(string-append res
|
||||
(case idx
|
||||
((0) "")
|
||||
((1) cell0-separator)
|
||||
(else cell-separator))
|
||||
(car srow)
|
||||
(if ansi? (ansi #:default) ""))
|
||||
(add1 idx))))
|
||||
right-border)
|
||||
res))))))
|
||||
|
||||
;; Creates table row delimiter based on column widths.
|
||||
(define (table-row-delimiter cws left line cross0 cross right)
|
||||
(string-append
|
||||
left
|
||||
(let cloop ((cws cws)
|
||||
(res "")
|
||||
(idx 0))
|
||||
(if (null? cws)
|
||||
res
|
||||
(cloop (cdr cws)
|
||||
(string-append res
|
||||
(case idx
|
||||
((0) "")
|
||||
((1) cross0)
|
||||
(else cross))
|
||||
(string-repeat line (car cws)))
|
||||
(add1 idx))))
|
||||
right))
|
||||
|
||||
;; Returns table row delimiter based on column widths, extracting
|
||||
;; line style from particular row of border style vector.
|
||||
(define (table-row-delimiter/styled tb cb0 cb cws svec srow)
|
||||
(define (sref i)
|
||||
(vector-ref svec (+ i (* srow 4))))
|
||||
(table-row-delimiter cws
|
||||
(if tb (sref 0) "")
|
||||
(sref 1)
|
||||
(if (or cb cb0) (sref 2) "")
|
||||
(if cb (sref 2) "")
|
||||
(if tb (sref 3) "")))
|
||||
|
||||
;; Converts given table to a string suitable for printing.
|
||||
(define (table->string tbl . args)
|
||||
(let ((table (table-prepare tbl)))
|
||||
(if (or (null? tbl)
|
||||
(null? (car tbl)))
|
||||
""
|
||||
(let* ((table-border (get-keyword #:table-border args (lambda () #f)))
|
||||
(row-border (get-keyword #:row-border args (lambda () #f)))
|
||||
(column-border (get-keyword #:col-border args (lambda () #f)))
|
||||
(row0-border (get-keyword #:row0-border args (lambda () #f)))
|
||||
(col0-border (get-keyword #:col0-border args (lambda () #f)))
|
||||
(border-style (get-keyword #:border-style args (lambda () (*table-border-style*))))
|
||||
(ansi? (get-keyword #:ansi args (lambda () #f)))
|
||||
(stylepair (assq border-style table-borders-lookup))
|
||||
(stylevec
|
||||
(if stylepair
|
||||
(cdr stylepair)
|
||||
(cdar table-borders-lookup)))
|
||||
(cell-borders (list (if table-border (vector-ref stylevec 4) "")
|
||||
(if (or column-border col0-border)
|
||||
(vector-ref stylevec 6) "")
|
||||
(if column-border (vector-ref stylevec 6) "")
|
||||
(if table-border (vector-ref stylevec 7) "")
|
||||
ansi?))
|
||||
(cws (map (compose ansi-string-length car) (car table))))
|
||||
(let loop ((rows table)
|
||||
(res (if table-border
|
||||
(list (table-row-delimiter/styled table-border
|
||||
col0-border
|
||||
column-border
|
||||
cws
|
||||
stylevec
|
||||
0))
|
||||
'()))
|
||||
(idx 0))
|
||||
(if (null? rows)
|
||||
(let ((res0 (if table-border
|
||||
(cons (table-row-delimiter/styled table-border
|
||||
col0-border
|
||||
column-border
|
||||
cws
|
||||
stylevec
|
||||
3)
|
||||
res)
|
||||
res)))
|
||||
(string-intersperse
|
||||
(flatten (reverse res0))
|
||||
"\n"))
|
||||
(let* ((res0
|
||||
(if (or (and row-border
|
||||
(> idx 0))
|
||||
(and row0-border
|
||||
(= idx 1)))
|
||||
(cons (table-row-delimiter/styled table-border
|
||||
col0-border
|
||||
column-border
|
||||
cws
|
||||
stylevec
|
||||
2)
|
||||
res)
|
||||
res))
|
||||
(res1
|
||||
(cons
|
||||
(apply table-row->lines
|
||||
(car rows)
|
||||
cell-borders)
|
||||
res0)))
|
||||
(loop (cdr rows)
|
||||
res1
|
||||
(add1 idx)))))))))
|
||||
|
||||
;; Performs module self-tests
|
||||
(define (table-tests!)
|
||||
(run-tests
|
||||
table
|
||||
(test-equal? string->rows (string->rows "asdf") '("asdf"))
|
||||
(test-equal? string->rows (string->rows "asdf\nqwer") '("asdf" "qwer"))
|
||||
(test-equal? string->rows (string->rows "\nasdf\nqwer") '("" "asdf" "qwer"))
|
||||
(test-equal? make-list-extender
|
||||
((make-list-extender 5) '("test"))
|
||||
'("test" "" "" "" ""))
|
||||
(test-equal? make-list-extender
|
||||
((make-list-extender 5 "x") '("test"))
|
||||
'("test" "x" "x" "x" "x"))
|
||||
(test-equal? table-rectangularize
|
||||
(table-rectangularize '(("x" "y" "z") ("a" "b") ("1" "2" "3" "4")))
|
||||
'(("x" "y" "z" "") ("a" "b" "" "") ("1" "2" "3" "4")))
|
||||
(test-equal? table-stringify
|
||||
(table-stringify '((1 2 3) (a b c) ("d")))
|
||||
'(("1" "2" "3") ("a" "b" "c") ("d")))
|
||||
(test-equal? table-prepare-cells
|
||||
(table-prepare-cells '(("x" "y" "z" "") ("a" "b" "" "") ("1" "2" "3" "4")))
|
||||
'((("x") ("y") ("z") ("")) (("a") ("b") ("") ("")) (("1") ("2") ("3") ("4"))))
|
||||
(test-equal? table-normalize-row
|
||||
(table-normalize-row '(("") ("a" "b")))
|
||||
'(("" "") ("a" "b")))
|
||||
(test-equal? table-column-widths
|
||||
(table-column-widths
|
||||
'((("x") ("y") ("zz") ("")) (("a") ("bcde") ("") ("")) (("123") ("2") ("3") ("4"))))
|
||||
'(3 4 2 1))
|
||||
(test-equal? table-normalize-cell
|
||||
(table-normalize-cell '("a" "bb" "ccc" "") 4)
|
||||
'("a " "bb " "ccc " " "))
|
||||
(test-equal? table-row-normalize-cells
|
||||
(table-row-normalize-cells
|
||||
'(("a") ("bb") ("ccc") (""))
|
||||
'(1 2 3 4))
|
||||
'(("a") ("bb") ("ccc") (" ")))
|
||||
(test-equal? table-normalize-columns
|
||||
(table-normalize-columns
|
||||
'((("a") ("bb") ("ccc") (""))
|
||||
(("") ("b") ("z") ("x"))))
|
||||
'((("a") ("bb") ("ccc") (" "))
|
||||
((" ") ("b ") ("z ") ("x"))))
|
||||
(test-equal? table-row->lines
|
||||
(table-row->lines '(("a ") ("bb") ("ccc") (" ")) "]" "|" "|" "[" #f)
|
||||
'("]a |bb|ccc| ["))
|
||||
(test-equal? table-row-delimiter
|
||||
(table-row-delimiter '(1 2 3 1) "/" "-" "+" "+" "\\")
|
||||
"/-+--+---+-\\")
|
||||
))
|
||||
|
||||
)
|
116
src/testing.scm
Normal file
116
src/testing.scm
Normal file
|
@ -0,0 +1,116 @@
|
|||
;;
|
||||
;; testing.scm
|
||||
;;
|
||||
;; Infrastructure for sipmle unit tests.
|
||||
;;
|
||||
;; ISC License
|
||||
;;
|
||||
;; Copyright 2023 Brmlab, z.s.
|
||||
;; Dominik Pantůček <dominik.pantucek@trustica.cz>
|
||||
;;
|
||||
;; 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 testing))
|
||||
|
||||
(module
|
||||
testing
|
||||
(
|
||||
test-eq?
|
||||
test-equal?
|
||||
test-exn
|
||||
test-true
|
||||
test-false
|
||||
run-tests
|
||||
)
|
||||
|
||||
(import scheme
|
||||
(chicken condition)
|
||||
(chicken format))
|
||||
|
||||
;; Evaluates body ... expressions with exception handler installed.
|
||||
(define-syntax with-handler
|
||||
(syntax-rules ()
|
||||
((_ handler body ...)
|
||||
(call-with-current-continuation
|
||||
(lambda (k)
|
||||
(with-exception-handler
|
||||
(lambda (x) (k (handler x)))
|
||||
(lambda () body ...)))))))
|
||||
|
||||
;; Test passes if the-test does not raise an exception and the result
|
||||
;; is eq? to expected-result.
|
||||
(define-syntax test-eq?
|
||||
(syntax-rules ()
|
||||
((_ name expression expected-result)
|
||||
(let ((result expression))
|
||||
(if (equal? result expected-result)
|
||||
(display ".")
|
||||
(error 'test-eq? (sprintf "~A expression=~S expected-result=~S result=~S"
|
||||
'name 'expression expected-result result)))))))
|
||||
|
||||
;; Test passes if the-test does not raise an exception and the result
|
||||
;; is equal? to expected-result.
|
||||
(define-syntax test-equal?
|
||||
(syntax-rules ()
|
||||
((_ name expression expected-result)
|
||||
(let ((result expression))
|
||||
(if (equal? result expected-result)
|
||||
(display ".")
|
||||
(error 'test-equal? (sprintf "~A expression=~S expected-result=~S result=~S"
|
||||
'name 'expression expected-result result)))))))
|
||||
|
||||
;; Test passes if the expression evaluates to #t, raises exception
|
||||
;; otherwise.
|
||||
(define-syntax test-true
|
||||
(syntax-rules ()
|
||||
((_ name expression)
|
||||
(let ((result expression))
|
||||
(if (eq? result #t)
|
||||
(display ".")
|
||||
(error 'test-true (sprintf "~A expression=~S result=~S"
|
||||
'name 'expression result)))))))
|
||||
|
||||
;; Test passes if the expression evaluates to #f, raises exception
|
||||
;; otherwise.
|
||||
(define-syntax test-false
|
||||
(syntax-rules ()
|
||||
((_ name expression)
|
||||
(let ((result expression))
|
||||
(if (eq? result #f)
|
||||
(display ".")
|
||||
(error 'test-true (sprintf "~A expression=~S result=~S"
|
||||
'name 'expression result)))))))
|
||||
|
||||
;; Passes if the-test raises an exception
|
||||
(define-syntax test-exn
|
||||
(syntax-rules ()
|
||||
((_ name the-test)
|
||||
(if (with-handler (lambda (x) #t)
|
||||
the-test #f)
|
||||
(display ".")
|
||||
(error 'unit-test name)))))
|
||||
|
||||
;; Displays test specification, evaluates all body ... expressions
|
||||
;; and prints "ok." at the and of the line if all succeed.
|
||||
(define-syntax run-tests
|
||||
(syntax-rules ()
|
||||
((_ name body ...)
|
||||
(let ()
|
||||
(display (sprintf "[test] ~A " 'name))
|
||||
body ...
|
||||
(print " ok.")))))
|
||||
|
||||
)
|
99
src/utils.scm
Normal file
99
src/utils.scm
Normal file
|
@ -0,0 +1,99 @@
|
|||
;;
|
||||
;; utils.scm
|
||||
;;
|
||||
;; Various utilities so that no external libraries are needed.
|
||||
;;
|
||||
;; ISC License
|
||||
;;
|
||||
;; Copyright 2023 Brmlab, z.s.
|
||||
;; Dominik Pantůček <dominik.pantucek@trustica.cz>
|
||||
;;
|
||||
;; 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 utils))
|
||||
|
||||
(module
|
||||
utils
|
||||
(
|
||||
filter
|
||||
string-repeat
|
||||
string-first+rest
|
||||
utils-tests!
|
||||
)
|
||||
|
||||
(import scheme
|
||||
(chicken base)
|
||||
(chicken string)
|
||||
(chicken irregex)
|
||||
testing)
|
||||
|
||||
;; Returns a list with elements matching pred? predicate.
|
||||
(define (filter pred? lst)
|
||||
(let loop ((lst lst)
|
||||
(res '()))
|
||||
(if (null? lst)
|
||||
(reverse res)
|
||||
(if (pred? (car lst))
|
||||
(loop (cdr lst)
|
||||
(cons (car lst) res))
|
||||
(loop (cdr lst)
|
||||
res)))))
|
||||
|
||||
;; Repeats given string.
|
||||
(define (string-repeat str rep)
|
||||
(let loop ((rep rep)
|
||||
(res '()))
|
||||
(if (> rep 0)
|
||||
(loop (sub1 rep)
|
||||
(cons str res))
|
||||
(string-intersperse res ""))))
|
||||
|
||||
;; Extracts first token and the rest as separate string
|
||||
(define (string-first+rest str)
|
||||
(let ((dm (irregex-search (irregex "[ \\t]" 'u) str)))
|
||||
(if dm
|
||||
(let* ((sep-idx (irregex-match-start-index dm))
|
||||
(key-str (substring str 0 sep-idx))
|
||||
(sep+val (substring str sep-idx))
|
||||
(val (irregex-replace (irregex "^[ \\t]*" 'u) sep+val "")))
|
||||
(cons key-str val))
|
||||
(cons str ""))))
|
||||
|
||||
|
||||
;; Performs utils module self-tests.
|
||||
(define (utils-tests!)
|
||||
(run-tests
|
||||
utils
|
||||
(test-equal? filter (filter odd? '(1 2 3 4)) '(1 3))
|
||||
(test-equal? filter (filter odd? '(2 4)) '())
|
||||
(test-equal? string-repeat
|
||||
(string-repeat "-" 4)
|
||||
"----")
|
||||
(test-equal? string-repeat
|
||||
(string-repeat "š" 4)
|
||||
"šššš")
|
||||
(test-equal? string-first+rest
|
||||
(string-first+rest "asdf rest")
|
||||
'("asdf" . "rest"))
|
||||
(test-equal? string-first+rest
|
||||
(string-first+rest "asdf rest test rest")
|
||||
'("asdf" . "rest test rest"))
|
||||
(test-equal? string-first+rest
|
||||
(string-first+rest "asdf")
|
||||
'("asdf" . ""))
|
||||
))
|
||||
|
||||
)
|
Loading…
Add table
Add a link
Reference in a new issue