#!/bin/sh
: ; exec klone $0 "$@"
; The above line finds the klone executable in the $PATH
;;Maintains a coffe log. add data at the end of this script and run it
;;to generate a mail, a web page, and archive this script via RCS
;;Can be customized for all kinds of servings (cans, bars...)
;;=============================================================================
;; Customize here:
;;=============================================================================
(setq generated-dir "/u/www/0/www/htdocs/files/koala/coffee") ;where this script is
;;rest of files will be found in generated-dir
(setq generated-html "coffee.html") ; genetared page
(setq generated-url "http://www.inria.fr/koala/coffee") ;script URL
(setq generated-text "coffee.txt") ; generated mail (also outputted)
(setq script-name "coffee") ; script name
(setq header-html "coffee-header.html") ; prepended to html page at generation
(setq title "Ampere Coffee Log") ; title of the page
(setq maintainer-id 'colas) ; maintainer id (ignored now)
(setq supplies-type
'(c "coffee" w "water" s "sugar" m "milk" u "cups" h "hardware" ? "???"))
(setq drank-verb "drank") ; consuming verb (past), "drank", "eat"
(setq cup-noun "cups") ; serving name, "cups", "cans", "bars"
(setq cup-value 2.50) ; value of a cup/serving
(setq currency-symbol "%0 F") ; "%0 F", "$%0"...
; %0 is expansed into the amount
(setq currency-name "francs") ; francs, dollars...
;;=============================================================================
;; Definitions
;;=============================================================================
;; This script is run to manage a cooperative consuming of good.
;; People put money ("Fund") into a common pool, or buy supplies which amount
;; is credited to their account, and each time an item (coffee cup, chocolate
;; bar, soda cans...) is consumed they put a mark on a log sheet.
;; By entering Funds, Supplies, and Cups consumed into this script you can
;; manage easily such a cooperative system.
;; The operation principle is to maintain 2 databases: one of abbrevs (a base
;; of people names and emails indexed by short IDs), and a base of events
;; "entries", list of events in chronological order
;; Events are of type Log, and are inputted at the end of this script as
;; arguments to the declare-log function (abbrevs are decalred with
;; the decalre-abbrevs function)
;; Events have types specified by a capital letter (funds, cups drank, date,
;; supplies bought...)
;; Running the script computes amounts and some stats, and output detailled
;; info on a web page
;; Author: Colas Nahaboo, http://www.inria.fr/koala/colas, Dec 1997.
(setq args (getopts "USAGE: %0 [options] "
("-v" () verbose "verbose operation")
("-t" id trace-id "trace changes to this id")
("-d" dir other-generated-dir "where to generate files, see script source")
("-email" () only-list-emails "Only lists non-() emails")
("-debug" () enter-debugger-on-error "enter klone debugger on error")
("-stackdump" () stackdump-on-error "verbose stack dump on error")
))
;; main logic
(defun main (&aux (list-of-id-and-amounts (list)))
;; data is inputted via declare-* functions
(setq *current-directory* generated-dir)
;; sort amount by debt
(dohash (id amount accounts)
(lappend list-of-id-and-amounts (list id amount)))
(sort list-of-id-and-amounts (lambda (x y) (compare (1 x) (1 y))))
;; then generate data from in memory bases
(generate-html list-of-id-and-amounts) ;complete html report
(generate-text list-of-id-and-amounts) ;summary report in ascii
)
(if enter-debugger-on-error (kdb t))
(if stackdump-on-error (stack-dump-on-error t))
(defstruct Abbrev id name email)
(defstruct Log
type ;atom: F S C D + -
id ;user or ()
(amount 0) ;amount to balance
value ;type-specific details
(date curdate) ;date of transaction
)
(defstruct Stats id name cups cupsval funds supplies) ;subtotals per id
(defvar stats (list)) ;list of Stats
(defun Log-name (l) (Abbrev-name (get abbrevs (Log-id l))))
(defvar abbrevs (list)) ;p-list of id Abbrev
(defvar entries (list)) ;log of all entries
(defvar abbrev-maxlen 0) ;max length of abbrevs
(defvar abbrev-maxnamelen 0) ;max length of full names
(defvar accounts (Hashtable ())) ; id balance
(if other-generated-dir (setq generated-dir other-generated-dir))
(defvar curdate "01/01/70")
(if trace-id
(defun account+ (id amount)
(if (= (String trace-id) (String id))
(PF " == %0: %1 + %2 ==> %3\n" id (get accounts id 0) amount
(+ amount (get accounts id 0))
))
(put accounts id (+ amount (get accounts id 0)))
)
(defun account+ (id amount) (put accounts id (+ amount (get accounts id 0))))
)
;;=============================================================================
;; Input processing functions
;;=============================================================================
;; Quoting functions to process and build databases in memory from data
;; entered into this script (at the end)
(defunq declare-abbrevs (&rest l &aux
id
(i 0)
non-first
)
(while (setq id (getn l i))
(lappend abbrevs id)
(setq abbrev-maxlen (max abbrev-maxlen (length id)))
(lappend abbrevs (make-Abbrev :id (get l i)
:name (setq name (get l (incf i)))
:email (get l (incf i))
))
(setq abbrev-maxnamelen (max abbrev-maxnamelen
(length (Abbrev-name (get abbrevs -1)))))
(incf i)
)
(if only-list-emails (progn
(dohash (id abbrev abbrevs)
(if (Abbrev-email abbrev) (progn
(if non-first
(PF ",\n ")
(setq non-first t)
)
(PF "%0" (Abbrev-email abbrev))
)))
(PF "\n")
(exit 0)
))
)
(defun log-get-id (l i) ;sets id as a side-effect
(if (not (getn abbrevs (setq id (getn l i))))
(fatal-error 1 "LOG: unknown ID %0\n" id)
))
(defunq declare-log (&rest l &aux
tag
id
n
(i 0)
)
(setq cup-value-orig cup-value)
(while (setq tag (getn l i)) (incf i)
(lappend entries
(if
(= tag 'F) (progn ; Funds: F id n
(log-get-id l i)
(account+ id (setq n (Number (get l (incf i)))))
(add-stats id Stats-funds n)
(make-Log :type tag :id id :amount n)
)
(= tag 'S) (progn ; Supplies: S id n t
(log-get-id l i)
(account+ id (setq n (Number (get l (incf i)))))
(add-stats id Stats-supplies n)
(make-Log :type tag :id id :amount n :value (get l (incf i)))
)
(= tag 'C) (progn ; Cups: C id n
(log-get-id l i)
(account+ id (setq n (- (* cup-value (Number (get l (incf i)))))))
(add-stats id Stats-cups (get l i))
(add-stats id Stats-cupsval (* cup-value (Number (get l i))))
(make-Log :type tag :id id :amount n :value (get l i))
)
(= tag 'D) (progn ; Date: D date
(setq curdate (get l i))
(make-Log :type tag)
)
(= tag '+) (progn ; Misc Funds: + id n desc
(log-get-id l i)
(account+ id (setq n (Number (get l (incf i)))))
(make-Log :type tag :id id :amount n :value (get l (incf i)))
)
(= tag '-) (progn ; Misc Debts: - id n desc
(log-get-id l i)
(account+ id (- (setq n (Number (get l (incf i))))))
(make-Log :type tag :id id :amount n :value (get l (incf i)))
)
(= tag 'V) (progn ; sets cup Value: V amount
(make-Log :type tag :value (setq cup-value (check-int (get l i))))
)
t (fatal-error 1 "Unknown log tag: %0, aborting\n" tag)
))
(incf i)
))
(defun add-stats (id slot value &aux
(s (catch 'Found (dolist (s stats)
(if (= (Stats-id s) id) (throw 'Found s)))
()
))
)
(if (not s) (lappend stats (setq s
(make-Stats :id id :name (Abbrev-name (get abbrevs id))
:cups 0 :cupsval 0 :funds 0 :supplies 0
))))
(slot s (+ value (slot s)))
)
;;=============================================================================
;; misc utils
;;=============================================================================
;; expand-real is included here for old klone versions
(setq expand-real:re (regcomp "^([0-9+-]+)[.]([0-9]*)$"))
(setq expand-real:re-zeros (regcomp "^0*$"))
(defun expand-real (x before after
&optional omit-zeros (decimal-point ".")
&aux s
(*real-precision* 1000)) ;avoid scientific notation
(if (regexec expand-real:re (setq s (PF String "%0" (Real x))))
(with (ip (regsub expand-real:re 1) fp (regsub expand-real:re 2))
(+
(make-string (- before (length ip)))
ip
(if (and omit-zeros (regexec expand-real:re-zeros fp))
""
(+ decimal-point (if (> (length fp) after) (subseq fp 0 after) fp)
(make-string (- after (length fp)) #\0)
))))
s
))
(setq comma:isfirst t)
(defun comma (&optional first)
(if first (setq comma:isfirst t)
(if (not comma:isfirst) ", " (progn (setq comma:isfirst ()) ""))
))
(defun incf-plist (l id n &aux prev)
(setq prev (get l id 0))
(put l id (+ prev n))
)
(defun html-mailto (email)
(if (and email (/= "" email))
(PF String "%0" email) "")
)
(defun currency-representation (amount)
(PF String currency-symbol amount)
)
(defun check-int (n)
(if (or (not (typep n Number)) (< n 0))
(fatal-error 1 "Invalid cup value: %0, aborting\n" n)
n
))
;;=============================================================================
;; report generating
;;=============================================================================
;;======================================================================== HTML
(defun generate-html (l &aux fd (tc 0) (tcv 0) (bal 0) (ts 0) (tf 0) (th 0)
(sd (list))
(rev-entries (list)))
;; Header (copied from file)
(sh cp ,header-html ,generated-html)
(setq fd (open generated-html :direction :output))
;; Debitors
(PF fd "
Date: %0
\nPeople owing money (need to pay!)
\n" curdate)
(dolist (e l) (if (< (1 e) 0)
(PF fd "| %0 | %1 | %2 |
\n"
(Abbrev-name (get abbrevs (0 e)))
(expand-real (1 e) 4 2 t)
(html-mailto (Abbrev-email (get abbrevs (0 e))))
)))
;; Creditors
(PF fd "
\n
People with positive accounts
\n")
(dolist (e l) (if (> (1 e) 0)
(PF fd "| %0 | %1 | %2 |
\n"
(Abbrev-name (get abbrevs (0 e)))
(expand-real (1 e) 4 2 t)
(html-mailto (Abbrev-email (get abbrevs (0 e))))
)))
;; Acounts at 0
(PF fd "
\n
People with empty accounts
\n")
(comma t)
(dolist (e l) (if (= (1 e) 0)
(PF fd "%1%0 %2" (Abbrev-name (get abbrevs (0 e))) (comma)
(html-mailto (Abbrev-email (get abbrevs (0 e))))
)))
;; Stats
(PF fd "\n
\nStats
\n")
(setq cup-value cup-value-orig)
(dolist (e entries)
(if (= 'C (Log-type e)) (progn
(incf tc (Log-value e))
(incf tcv (* cup-value (Log-value e)))
))
(if (= 'V (Log-type e)) (setq cup-value (Log-value e)))
(if (and (= 'S (Log-type e)) (= 'h (Log-value e)))
(incf th (Log-amount e))
)
)
(PF fd "Total %1 %2: %0\n
" tc cup-noun drank-verb)
(dohash (id amount accounts) (incf bal amount))
(PF fd "Total money in pool: %0 (sum of user accounts current balance)\n
" bal)
(dolist (e entries) (if (= 'S (Log-type e)) (progn
(incf ts (Log-amount e))
(incf-plist sd (Log-value e) (Log-amount e))
)))
(PF fd "Total cost of supplies (excluding hardware, which amounts to %1): %0, composed of:\n"
(- ts th) th)
(dohash (type amount sd)
(if (/= 'h type)
(PF fd "- %0: %1 (%2%3)\n" (get supplies-type type) amount
(Int (* 100 (/ amount (- ts th)))) "%")
))
(PF fd "
")
(PF fd
"Mean cost of a cup: %0. With hardware included: %1\n
"
(expand-real (/ (Real (- ts th)) tc) 4 2)
(expand-real (/ (Real ts) tc) 4 2))
(dolist (e entries) (if (= 'F (Log-type e)) (incf tf (Log-amount e))))
(PF fd "Total money brought by people in cash: %0\n
" tf)
(PF fd "Profit: %0 (cup_values - supplies)\n
" (- tcv ts))
;; Per-user stats
(PF fd "\n
\nPer-user Stats
\n")
(PF fd "| name | %0 | funds | supplies |
\n"
cup-noun)
;;sort stats per cups
(sort stats (lambda (s1 s2) (compare (Stats-cups s2) (Stats-cups s1))))
(dolist (s stats)
(PF fd "| %0 | %1 (%2) | %3 | %4 |
\n"
(Stats-name s) (Stats-cups s)
(currency-representation (Stats-cupsval s))
(Stats-funds s) (Stats-supplies s)
)
)
(PF fd "
")
;; Detailed log
(PF fd "\n
\nDetailed Log
\n")
(dolist (e entries) (insert rev-entries 0 e))
(setq last-printed-date "")
(dolist (e rev-entries)
(if (/= last-printed-date (Log-date e)) (progn
(PF fd "\n%0
\n" (Log-date e))
(setq last-printed-date (Log-date e))
(comma t)
))
(if
(= 'F (Log-type e))
(PF fd "%2%0 payed %1" (Log-name e)
(currency-representation (Log-amount e)) (comma)
)
(= 'C (Log-type e))
(PF fd "%2%0 %3 %1 %4" (Log-name e) (Log-value e) (comma)
drank-verb cup-noun)
(= 'S (Log-type e))
(PF fd "%2%0 bought for %1 of %3" (Log-name e)
(currency-representation (Log-amount e))
(comma) (get supplies-type (Log-value e))
)
(= '+ (Log-type e))
(PF fd "%2%0 payed %1 for %3" (Log-name e)
(currency-representation (Log-amount e)) (comma)
(Log-value e)
)
(= '- (Log-type e))
(PF fd "%2%0 owes the community %1 for %3" (Log-name e)
(currency-representation (Log-amount e)) (comma)
(Log-value e)
)
(= 'V (Log-type e))
(PF fd "%2Cup value set to %0" (currency-representation (Log-value e))
() (comma)
)
))
;; End
(PF fd "