#!/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

\n

People owing money (need to pay!)

\n" curdate) (dolist (e l) (if (< (1 e) 0) (PF fd "\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 "
%0%1 %2
\n

People with positive accounts

\n") (dolist (e l) (if (> (1 e) 0) (PF fd "\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 "
%0%1 %2
\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


\n

Stats

\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") (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
\n

Per-user Stats

\n") (PF fd "\n" cup-noun) ;;sort stats per cups (sort stats (lambda (s1 s2) (compare (Stats-cups s2) (Stats-cups s1)))) (dolist (s stats) (PF fd "\n" (Stats-name s) (Stats-cups s) (currency-representation (Stats-cupsval s)) (Stats-funds s) (Stats-supplies s) ) ) (PF fd "
name%0fundssupplies
%0%1 (%2)%3%4
") ;; Detailed log (PF fd "\n
\n

Detailed 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 "\n") ) ;;================================================================ ASCII report (defun generate-text (l &aux (fd (open generated-text :direction :io :if-exists :supersede)) (to-pay (list)) last-id ) (dolist (e l) (if (< (1 e) 0) (lappend to-pay (0 e)))) ;compute debitors (setq last-id (get to-pay -1)) (PF fd "To:") (dolist (id to-pay) (PF fd "\t%0%1" (Abbrev-email (get abbrevs id)) (if (not (eq last-id id)) ",\n" "\n") )) (PF fd "\n%0 - %1\n\n" title curdate) (PF fd "PEOPLE NEEDING TO PAY / DOIVENT DE L'ARGENT:\n") (PF fd "============================================\n") (PF fd " %0%1 %2\n\n" "NAME / NOM" (make-string (- abbrev-maxnamelen (length "NAME / NOM"))) (toupper currency-name) ) (dolist (e l) (if (< (1 e) 0) (PF fd " %0%2 %1\n" (Abbrev-name (get abbrevs (0 e))) (expand-real (- (1 e)) 4 2 t) (make-string (- abbrev-maxnamelen (length (Abbrev-name (get abbrevs (0 e)))))) ))) (PF fd "\nDetails on %0\n\n" generated-url) ;; echo on stdout (close fd) (sh cat ,generated-text) ;; archive via RCS this script (sh ci -l -q ,script-name < /dev/null) ) ;;############################################################################ ;;######################## ENTER DATA HERE ############################### ;;############################################################################ (declare-abbrevs ;; ID FullName Email_or_() koala "Koala Fund" () ; caisse noire koala howcome "Hakon Lie" "Hakon.Lie@sophia.inria.fr" colas "Colas Nahaboo" "colas@sophia.inria.fr" ml "Marie-Line Ramfos" "Marie-Line.Ramfos@sophia.inria.fr" chris "Chris Lilley" "Chris.Lilley@sophia.inria.fr" bert "Bert Bos" "Bert.Bos@sophia.inria.fr" yves "Yves Lafon" "Yves.Lafon@sophia.inria.fr" phk "Philippe Kaplan" "Philippe.Kaplan@sophia.inria.fr" jml "Jean-Michel Léon" () ; Jean-Michel.Leon@sophia.inria.fr abaird "Anselm Baird-Smith" () ; abaird w3c "Guests w3c" "Daniel.Dardailler@sophia.inria.fr" koala "guests koala" "colas@sophia.inria.fr" jmi "Janet Bertot" "Janet.Bertot@sophia.inria.fr" boy "Stéphane Boyera" "Stephane.Boyera@sophia.inria.fr" osofia "Olivier Sofia" () ; osofia arnaud "Arnaud Le Hors" "Arnaud.Le_hors@sophia.inria.fr" benoit "Benoit Mahe" "Benoit.Mahe@sophia.inria.fr" roy "Roy Platton" () jpo "Jean-Philippe Orsini" () ; jporsini@sophia.inria.fr eka "Thierry Kormann" "Thierry.Kormann@sophia.inria.fr" plh "Philippe Le Hegaret" "plehegar@sophia.inria.fr" ian "Ian Jacobs" () ; ijacobs ext "Externals" () janne "Janne Saarela" jsaarela isa "Isabelle Robert" irobert peto "Pierre Clerissi" pcleriss helene "Hélène Le Guen" hleguen josef "Josef Dietl" "Josef.Dietl@sophia.inria.fr" ) (declare-log ; chronological order D "1 Jan 97" V 2.50 S koala 3000 h ; achat de la machine sur fonds koala D "1 Feb 97" C howcome 33 C colas 50 C chris 70 C bert 68 C yves 16 C phk 26 C jml 30 C abaird 37 C w3c 8 C jmi 7 C boy 2 C ml 5 C osofia 10 C benoit 10 C arnaud 14 C roy 5 S colas 475 c S chris 20 c S chris 12 m S chris 8 m D "1 Apr 97" F howcome 100 F bert 200 F yves 40 F phk 100 F jml 75 F abaird 92.5 F jmi 50 F boy 10 F ml 20 F osofia 25 F benoit 25 F arnaud 35 F roy 15 D "1 May 97" C howcome 19 C colas 20 C chris 31 C bert 12 C yves 7 C phk 7 C abaird 8 C jmi 8 C ml 12 C osofia 5 C benoit 3 C arnaud 14 C roy 27 C ian 17 S colas 94 c S colas 32 w S chris 3 m S chris 5 s D "15 May 97" F howcome 100 F chris 300 F yves 20 F ml 30 F osofia 25 F roy 80 F ian 56 D "1 Jun 97" C howcome 26 C colas 30 C chris 39 C bert 51 C yves 3 C phk 20 C abaird 34 C jmi 1 C boy 2 C ml 9 C osofia 31 C benoit 15 C arnaud 44 C roy 6 C ian 11 C jpo 21 C eka 29 C plh 47 S colas 49 w S chris 13 s S bert 132 c S phk 18 w S arnaud 70 c S eka 6 w S plh 36 c D "1 Jul 97" F yves 5 F abaird 120 F w3c 20 F ml 20 F osofia 70 F benoit 63 F arnaud 100 F ian 30 F jpo 52 F eka 100 F plh 100 C osofia 2 ;cloture de compte D "1 Aug 97" C howcome 7 C colas 13 C chris 9 C bert 13 C phk 4 C abaird 6 C ml 6 C benoit 1 C arnaud 9 C jpo 13 C eka 16 C plh 23 S colas 107 c S colas 12 w S ml 40 c S arnaud 40 c D "11 Sep 97" F chris 11.5 F phk 50 F jpo 35 F plh 40 F ext 20 D "1 Oct 97" C ext 8 D "1 Dec 97" ;feuille "Nov 97" C colas 50 C bert 30 C arnaud 28 C benoit 20 C ml 28 C phk 12 C janne 28 C chris 31 C isa 14 C yves 14 C jpo 9 C jmi 4 C boy 1 C plh 9 C eka 1 S colas 130 w S colas 190 c S janne 11.4 w S chris 3 m ;; start of daily use D "12 Dec 97" F jpo 50 D "15 Dec 97" F bert 200 D "17 Dec 97" F ml 40 F boy 5 D "19 Dec 97" F janne 60 D "5 Jan 98" F plh 50 D "12 Jan 98" F yves 36 D "15 Jan 98" C bert 22 C colas 33 C janne 12 C plh 33 C arnaud 26 C ml 21 C phk 11 C chris 17 C yves 4 C benoit 14 C jpo 32 C eka 5 C boy 3 C howcome 2 C isa 2 C ian 5 S plh 57.3 c S plh 18.9 w S phk 18.9 w S ian 38 c F arnaud 120 F yves 10 F benoit 70 F phk 20 F jpo 51 F eka 50 D "16 Jan 98" F isa 50 D "9 Feb 98" F howcome 20 D "10 Feb 98" C plh 24 C jpo 31 C benoit 13 C arnaud 11 C ml 15 C colas 14 C yves 5 C bert 16 C phk 5 C jmi 1 C chris 9 C isa 5 C eka 10 C janne 4 C boy 1 C howcome 2 F benoit 100 F ml 150 F plh 40 F yves 12 D "13 Feb 98" F boy 20 D "11 Mar 98" S yves 114.6 c S yves 37.8 w D "17 Mar 98" C bert 30 C colas 35 C plh 31 C jpo 19 C chris 10 C benoit 23 C phk 12 C eka 14 C howcome 2 C ml 30 C janne 9 C isa 8 C arnaud 20 C boy 1 C yves 16 S phk 18.9 w F jpo 130 F phk 50 F ml 100 D "30 Mar 98" F arnaud 100 F eka 50 F bert 100 D "3 Apr 98" F jmi 10 F janne 61.10 D "15 Apr 98" - chris 86.09 "custom taxes for parcel arrived at INRIA" D "25 Apr 98" V 1.5 D "29 Apr 98" C ml 39 C colas 30 C eka 20 C plh 49 C arnaud 15 C benoit 27 C isa 17 C bert 31 C phk 11 C yves 6 C peto 30 C janne 9 C helene 15 C jmi 1 C jpo 20 F jpo 24.5 ;; offerts par colas S colas 75.6 w S colas 114.6 c S phk 76.4 c S peto 18.9 c D "30 Apr 98" F eka 50 F koala -3000 D "6 May 98" F plh 200 D "7 May 98" F isa 100 D "15 May 98" S colas 94.5 w S colas 191 c F colas -700 D "20 May 98" F howcome 100 D "28 May 98" C plh 28 C helene 25 C colas 18 C ml 17 C benoit 11 C yves 5 C bert 22 C peto 18 C isa 4 C phk 5 C eka 4 C janne 10 C chris 4 C arnaud 1 C howcome 7 S plh 18 w D "23 Jun 98" F peto 100 D "25 Jun 98" F helene 150 D "30 Jun 98" S bert 18.90 w S colas 94.5 w S colas 191 c C eka 9 C phk 11 C ml 23 C helene 30 C peto 20 C benoit 17 C plh 51 C josef 3 C isa 8 C janne 10 C yves 6 C chris 5 C bert 5 C arnaud 5 C colas 13 C howcome 2 D "1 Jul 98" F josef 5 F bert 50 F janne 50 ) ;; F id n = funds deposited ;; S id n t = supplies bought (t: coffee, water, sugar, milk, cUps, ?) ;; C id n = number of drank cups ;; D date = freeform string (main) ;;; EMACS MODES ;;; Local Variables: *** ;;; mode:lisp *** ;;; End: ***