SubscribeDownload: xcMainXMLTagsEditHistoryDiscussion

  1. Introduction
  2. Status
  3. Example session
  4. To do
  5. License
  6. Dependencies
  7. Suffixes for input and output
    1. IEC suffixes
    2. Temperature
    3. Length
    4. Mass and weight
    5. Time
    6. List of all suffixes
  8. Lexer function
  9. Evaluation of expressions
    1. Top-level parser
    2. List of binary operators
    3. Evaluation of binary operators
    4. Atoms
  10. Output
    1. Numbers with suffixes
    2. Literal output
    3. Time intervals
    4. Default output format
  11. Program
    1. Command line arguments
    2. RC File
    3. Main function
  12. Author

Introduction

xc is an interactive command-line calculator that can be used as a replacement for the bc and units Unix programs. It was designed with the following goals:

  1. Use an as “universal” as possible syntax for input expressions. For example, use infix binary operators and express procedure application as “proc(arg0, ..., argn)”.
  2. Make the syntax require very little typing. In other words, make it fast to type a lot of expressions. Part of this is recognizing numbers in multiple formats. For instance, understand time intervals specified as “5d3h20s” or “5:3:0:20” and convert that to the number of seconds (442820); similarly for things like “3Gi” (3*2^30) or “3G” (3*10^9).
  3. Support different formatters for printing the output back to the user in a human-readable (but context-dependant) manner.
  4. Support the evaluation of procedures defined in Scheme and the use of variables to capture and reuse the results of expressions.

This file describes the implementation —including the full source code and unit tests— and usage of xc.

xc is executed using the Chicken implementation of the Scheme programming language.

Status

As of 2008-12-14, xc is still evolving.

The amount of units supported is still relatively small and there could still be bugs. Feel free to add support for more units by adding them to the *suffixes* list (see below).

Also, the set of tests on the code is not as exhaustive as it should be. You can help by adding more tests (“examples”) to this page.

Use at your own risk.

Example session

The following is an example session. For readability, a “=> ” has been prepended to each result.

# Basic example:
29 + 1
=> 30.00

# You can specify units such as Pi or M.  The output is adjusted
# to use the right suffix:
8.3Pi/12M
=> 742.67Mi

# If you prefer, you can change the output:
output_suffix("1000")
=> #<procedure (? num)>

# Now the output is in "M", not in "Mi":
8.3Pi/12M
=> 778.75M

# Or you can request no conversion, just literal printing:
output_num=output_float
=> #<procedure (output-int num)>

8.3Pi/12M
=> 778747435.57

# Back to the default:
output_suffix("2")
=> #<procedure (? num)>

# Most normal binary operators are supported:
days=1+(2*3^4/5)^6/(77%8)
=> 220.65Mi

# Storing results in variables and reusing them:
result=2*days/1Mi
=> 441.30

result /= 10
=> 44.13

# Calling Scheme-defined procedures:
mean(10, 20, 30, result)
=> 26.03

# Operating on lists:
cadr(map(square,list(1,2,3)))
=> 4.00

# Operating on strings and files (it is assumed that /tmp/test.txt
# exists and begins with a line of 3 characters):
string_length("foo" + read_line(open_input_file("/tmp/test.txt")))
=> 6.00

# Working with temperatures:
output_suffix("tempc")
=> "tempc"

# Conversion of Fahrenheit to Celsius:
75tempf
23.89tempc

# Working with units of length.  "mass" can also be used.
output_suffix("length")
=> "length"

# Conversion:
1ft
=> 0.30m

# Conversion.  Note that this time the answer is in Km, not in m:
12mile + 10000ft
=> 22.36Km

# You can specify a particular suffix and it will be used:
output_suffix("m")
=> "m"

12mile + 10000ft
=> 22360.13m

mean” and “square” are defined as follows in a file (~/.xcrc) loaded at startup:

(define (mean . args)
  (/ (apply + args) (length args)))

(define (square x) (* x x))

To do

  • Support a syntax for scheme expressions.
  • The conversion of units based on suffixes should not be done by the lexer. Instead, the suffix should be returned and parse-input-atom (or a wrapper) should take care of that. This would allow the evaluation of expressions such as “(72.5 + 5)tempf”.

License

#!/usr/local/bin/csi -s
; Copyright 2008 Alejandro Forero Cuervo <azul@freaks-unidos.net>
; All Rights Reserved
;
; This code is available under the GPLv3 license.
;
; The authoritative source for this program, where new versions may be
; available, is:
;
;   http://wiki.freaks-unidos.net/xc

Dependencies

xc uses SRFI-40 streams and the stream-parser module for evaluating input expressions.

For output, it uses format-modular.

(use stream-parser stream-ext srfi-40 format-modular args embedded-test)

We also use the following standard Chicken libraries (which you won't have to install separately):

(use srfi-1 posix)

We use the readline library if it's available, setting the prompt to the empty string.

(when (extension-information 'readline)
  (use readline)
  (current-input-port (make-gnu-readline-port ""))
  (gnu-history-install-file-manager (string-append (or (getenv "HOME") ".") "/.xc_history")))

We use the numbers library if it's available, which brings us the full numerical tower:

(when (extension-information 'numbers)
  (use numbers))

Suffixes for input and output

We want to make it possible for the user to specify and parse numbers in a human-readable format. We have a system of “suffixes” that allows us to convert a number followed by one to a number (eg. 3Pi is the number 3*2^50) and from a number back to a human-readable format (eg. the number 3*2^50 is printed as 3Pi).

We define a list with the suffixes for numbers that we support. Each entry in the list has the form “(name input output)”, where “input” and “output” are procedures used to convert a number followed by the suffix name into a number (eg. 33*2^50 for the suffix Pi) and a number into a number that could be printed followed by the suffix (eg. 3*2^503 for the suffix Pi) respectively. In other words, output is always the inverse of input.

Given that most (but not all: see the Fahrenheit conversion, for example) of the time the conversion will be simply a multiplication or division, we provide a procedure that builds entries of this type:

(define (suffix-rate symbol rate)
  (list symbol (cut * <> rate) (cut / <> rate)))

IEC suffixes

First we list with the IEC suffixes:

(define *suffixes-iec-first-char* '(K M G T P E Z Y))

Based on this, we provide a function that receives a given suffix as a symbol (eg. “m”) and returns a list with one entry for each element of *suffixes-iec-first-char* prepending it to the suffix (eg. “Km”, “Mm”, etc.).

The optional parameter base is the multiplier for each entry (eg. to go from “m” to “Km”, from “Km” to “Mm”, etc.). In the usual case this will be the default of 1000.

Every entry of the list has the “(name input output)” format described above.

(define (build-suffixes-list suffix . rest)
  (let-optionals rest ((base 1000))
    (map (lambda (x y)
           (suffix-rate (string->symbol (format #f "~A~A" x suffix))
                        (expt base y)))
         *suffixes-iec-first-char*
         (iota 10 1))))

Now we declare lists with the suffixes themselves and with the binary-versions of the suffixes (where they are powers of 1024):

(define *suffixes-1000* (build-suffixes-list ""))
(define *suffixes-1024* (build-suffixes-list "i" 1024))

Temperature

(define *suffixes-temperature*
  `((tempk ,identity ,identity)
    (tempc ,(cut + <> 273.15) ,(cut - <> 273.15))
    (tempf ,(lambda (x) (* (+ x 459.67) (/ 5 9)))
           ,(lambda (x) (- (* x (/ 9 5)) 459.67)))))

Length

(define *suffixes-1000-length* (build-suffixes-list "m"))

(define *suffixes-length*
  `((m ,identity ,identity)
    ,@*suffixes-1000-length*
    ,(suffix-rate 'inches 0.02540)
    ,(suffix-rate 'feet 0.30480)
    ,(suffix-rate 'yards 0.9144)
    ,(suffix-rate 'miles 1609.344)
    ,(suffix-rate 'lightyears (* 9.461 (expt 10 15)))))

Mass and weight

While the most commonly used unit for mass is the kilogram, we use grams internally so that conversions based on build-suffixes-list will work. This is probably something that should be fixed.

(define *suffixes-1000-mass* (build-suffixes-list "g"))

(define *suffixes-mass-and-weight*
  `((g ,identity ,identity)
    ,@*suffixes-1000-mass*
    ,(suffix-rate 'tonne 1000000)
    ,(suffix-rate 'pound 0453.59237)))

Time

We define a few time units.

We should probably extend the system of suffixes to allow a list of names for a given suffix, so that we can use singular versions of the names.

(define *suffixes-time*
  `((seconds ,identity ,identity)
    ,(suffix-rate 'minutes    (* 60))
    ,(suffix-rate 'hours      (* 60 60))
    ,(suffix-rate 'days       (* 60 60 24))
    ,(suffix-rate 'weeks      (* 60 60 24 7))
    ,(suffix-rate 'months     (* 60 60 24 29.530589))
    ,(suffix-rate 'years      (* 60 60 24 365.25))\
    ,(suffix-rate 'decades    (* 60 60 24 365.25 10))
    ,(suffix-rate 'centuries  (* 60 60 24 365.25 10 10))
    ,(suffix-rate 'milleniums (* 60 60 24 365.25 10 10 10))))

List of all suffixes

Finally we build a list with all the suffixes we support:

(define *suffixes*
  (append
    *suffixes-1024*
    *suffixes-1000*
    *suffixes-temperature*
    *suffixes-length*
    *suffixes-mass-and-weight*
    *suffixes-time*))

Lexer function

Lets now implement our lexer function. It receives a stream of characters and consumes as little tokens as possible, returning them as a result.

We support the following rules:

  • A number optionally followed by a suffix (in *suffixes*).
  • Whitespace or comments, which we just ignore.
  • Certain characters we return as they are: newlines, parenthesis, binary operators and the equal sign.
  • A quoted string such as “"foo"“.
  • A “symbol”: an unreserved character other than a number followed by any unreserved characters.

There is one tricky part: if we find a “-”, we don't know whether it is part of a number (eg. “-5”) or if it should be interpreted as the binary operator for subtraction. As such, we always return this as the character “#\-” and let the parser deal with that.

(define (lexer str)
  (parse-all
    str
    (lambda () (error "lexer error"))
    (lambda (str fail parsed)
      (parse-token str fail parsed
        (((bind number (+all char-numeric?) (? #\. (+all char-numeric?)))
          (? (bind suffix (all char-alphabetic?))
             (assert (assoc (stream->symbol suffix) *suffixes*))))
         (stream
           ((if (stream-null? suffix)
              identity
              (cadr (assoc (stream->symbol suffix) *suffixes*)))
            (stream->number number))))
        (((or ((+all #\space))
              (#\# (all (not #\newline)))))
         stream-null)
        (((bind special (or #\newline #\( #\) #\* #\+ #\- #\% #\/ #\^ #\, #\=)))
         special)
        ((#\" (bind text (all (or (#\\ #\") ((not #\"))))) #\")
         (stream (stream->string text)))
        (((bind symbol
                (not (or char-whitespace? char-numeric? #\( #\) #\* #\+ #\- #\% #\/ #\^ #\, #\=))
                (all (not (or char-whitespace? #\( #\) #\* #\+ #\- #\% #\/ #\^ #\, #\=)))))
         (stream (stream->symbol (stream-translate symbol #\_ #\-))))))))

Now a function to simplify testing:

(define lexer-test (compose stream->list lexer string->stream))

Examples:

(lexer-test "  1  \n")
=> '(1 #\newline)
(lexer-test "# comments\n# comments\n1\n# comments\n")
=> '(#\newline #\newline 1 #\newline #\newline)
(lexer-test "1M 1Mi")
=> `(,(expt 10 6) ,(expt 2 20))
(lexer-test "1 + 2 + x\n3 * (4 - 5)\n")
=> '(1 #\+ 2 #\+ x #\newline 3 #\* #\( 4 #\- 5 #\) #\newline)
(lexer-test "")
=> '()
(lexer-test "x=open_input_file")
=> '(x #\= open-input-file)
(lexer-test "1tempk 1tempc 1Km")
=> '(1.00 274.15 1000)

Evaluation of expressions

Top-level parser

Here we take care of expressions prefixed with a symbol and an equal sign, which assigns the value to the symbol. We also consume empty lines (comments and whitespace have already been consumed by the lexer).

We use parse-input-expr for the actual evaluation of expressions.

(define (parse-input str)
  (parse-all
    str
    (lambda () (error "parser error"))
    (lambda (str fail parsed)
      (parse-token str fail parsed
        ((#\newline)
         stream-null)
        (((all (bind name symbol?)
               (bind-accum (names '()) cons name)
               (bind op (? binary-operator?))
               (bind-accum (modifiers '()) cons op)
               #\=
               )
          (bind value (rule-apply (cut parse-input-expr *binary-operators* <...>)))
          #\newline)
         (stream
           (fold
             (lambda (name modifier value)
               (let ((new-value
                       ((if (stream-null? modifier)
                          identity
                          (cut
                            (cadr (assoc (stream-car modifier)
                                         (concatenate (map cdr *binary-operators*))))
                            (eval (stream-car name))
                            <>))
                        value)))
                 (eval `(set! ,(stream-car name) ',new-value))
                 new-value))
             (stream-car value)
             names
             modifiers)))
        (((all (not #\newline))
          #\newline)
         (warning "Evaluation of expression failed")
         stream-null)))))

List of binary operators

We list the binary operators that we support, with their associativity, sorted by precedence. All of them are mapped to the standard Scheme functions except for +, which we overload to support addition of strings.

(define (xc-+ . args)
  (cond
    ((every number? args) (apply + args))
    ((every string? args) (apply string-append args))))

(define *binary-operators*
  `((right (#\^ ,expt))
    (left (#\* ,*) (#\/ ,/) (#\% ,modulo))
    (left (#\+ ,xc-+) (#\- ,-))))

We also create a function to evaluate if an object is a binary operator:

(define binary-operator?
  (cute member <>
        (concatenate
          (map (compose (cut map car <>) cdr) *binary-operators*))))

Evaluation of binary operators

parse-input-binary receives a list of binary operators ordered by precedence (in the format of *binary-operators*: grouped by precedence and declaring their associativity) and evaluates all of them, returning the resulting expression.

(define (parse-input-expr ops str fail parsed)
  (parse-token str fail parsed
    (((bind expr
            (rule-apply parse-input-atom)
            (all (bind op-inner binary-operator?)
                 (assert (assoc (stream-car op-inner) (cdar ops)))
                 (rule-apply parse-input-atom)))
      (bind rest
            (? (bind op-rest binary-operator?)
               (assert (not (assoc (stream-car op-rest) (cdar ops))))
               (rule-apply (cut parse-input-expr (list (car ops)) <...>)))))
     (let ((value
             (let loop ((expr expr))
               (cond
                 ((stream-null? (stream-cdr expr))
                  (stream-car expr))
                 ((eq? (caar ops) 'right)
                  ((cadr (assoc (stream-cadr expr) (cdar ops)))
                   (stream-car expr)
                   (loop (stream-cddr expr))))
                 ((eq? (caar ops) 'left)
                  (loop
                    (stream-cons
                      ((cadr (assoc (stream-cadr expr) (cdar ops)))
                       (stream-car expr)
                       (stream-caddr expr))
                      (stream-cdddr expr))))))))
       (if (null? (cdr ops))
         (stream-cons value rest)
         (receive (result stream fail parsed)
                  (parse-input-expr
                    (cdr ops)
                    (stream-cons value rest)
                    fail
                    parsed)
           result))))))

Now a function to simplify testing:

(define (parse-input-expr-test str)
  (stream->list
    (parse-all
      (list->stream str)
      (lambda () (error "test parse-input-expr error"))
      (cut parse-input-expr *binary-operators* <...>))))

Examples:

(parse-input-expr-test '(1 #\- 1 #\+ 1))
=> '(1)
(parse-input-expr-test '(4 #\^ 3 #\^ 2))
=> '(262144)
(parse-input-expr-test '(2 #\* #\( 1 #\+ 1 #\) #\+ 1))
=> '(5)
(parse-input-expr-test '(1 #\+ 2 #\* 3))
=> '(7)
(parse-input-expr-test '(3 #\* 2 #\+ 1 #\^ 4))
=> '(7)
(parse-input-expr-test '("foo" #\+ "bar"))
=> '("foobar")

Atoms

Now the definition of an atom.

We have rules for these cases:

  • A parenthized expression.
  • A negative number.
  • Procedure application, of the form “proc(arg0, ..., argn)”.
  • A symbol lookup.
(define (parse-input-atom str fail parsed)
  (parse-token str fail parsed
    ((#\(
      (bind sub (rule-apply (cut parse-input-expr *binary-operators* <...>)))
      #\))
     sub)
    ((#\-
      (bind obj number?))
     (stream (- (stream-car obj))))
    (((bind func symbol?)
      #\(
      (? (bind first-arg (rule-apply (cut parse-input-expr *binary-operators* <...>)))
         (* #\,
            (bind arg (rule-apply (cut parse-input-expr *binary-operators* <...>)))
            (bind-accum (args '()) cons arg)))
      #\))
     (stream
       (condition-case
         (apply
           (eval (stream-car func))
           (if (stream-null? first-arg)
             '()
             (map stream-car (cons first-arg (reverse args)))))
         ((exn)
          (warning "Exception during invocation of procedure" (stream-car func))
          (fail)))))

    (((bind sym symbol?))
     (stream
       (condition-case
         (eval (stream-car sym))
         ((exn)
          (warning "Unable to evaluate symbol" (stream-car sym))
          (fail)))))

    (((bind obj (or number? port? procedure? string?
                    stream? boolean? pair? null?)))
     obj)))

Now a function to simplify testing:

(define (parse-input-atom-test str)
  (stream->list
    (parse-all
      (list->stream str)
      (lambda () (error "test parse-input-atom error"))
      parse-input-atom)))

Examples:

(parse-input-atom-test '(1892))
=> '(1892)
(parse-input-atom-test '(#\( 1 #\+ 1 #\)))
=> '(2)
(parse-input-atom-test '(xc-+ #\( 1 #\, 1 #\)))
=> '(2)
(parse-input-atom-test `(,xc-+))
=> `(,xc-+)
(parse-input-atom-test '(xc-+))
=> `(,xc-+)

Output

Numbers with suffixes

First we define a procedure that, given a particular entry from the *suffixes* list, will output the results using it:

(define output-digits 2)

(define (output-with-suffix suffix num)
  (format #t "~,VF~A~%"
          output-digits
          ((caddr suffix) num)
          (car suffix)))

We also define a “smarter” procedure that will find the suitable suffix from a list, depending the size of the input.

(define (output-with-auto-suffixes limit suffixes)
  (lambda (num)
    (let loop ((suffixes suffixes) (num num))
      (cond
        ((or (< num limit)
             (null? (cdr suffixes)))
         (format #t "~,VF~A~%"
                 output-digits
                 num
                 (caar suffixes)))
        (else
          (loop (cdr suffixes) (/ num limit)))))))

Based on that, we build a list of “systems” of suffixes that we support. The entries in the list are of the form “(name proc)”, where “name” is a symbol naming the system. We take care to make the set of names disjunct from the set of prefixes, so that a given name can uniquely specify either a system of suffixes or a particular suffix.

(define output-base-1024 (output-with-auto-suffixes 1024 (cons (list "") *suffixes-1024*)))
(define output-base-1000 (output-with-auto-suffixes 1000 (cons (list "") *suffixes-1000*)))
(define output-length (output-with-auto-suffixes 1000 (cons (list "m") *suffixes-1000-length*)))
(define output-mass   (output-with-auto-suffixes 1000 (cons (list "g") *suffixes-1000-mass*)))

(define *output-suffixes*
  `((|10| ,output-base-1000)
    (|1000| ,output-base-1000)
    (|2| ,output-base-1024)
    (|1024| ,output-base-1024)
    (length ,output-length)
    (mass ,output-mass)))

Examples:

(with-output-to-string (cut output-base-1024 1023))
=> "1023.00\n"
(with-output-to-string (cut output-base-1024 (* 2 (expt 1024 3))))
=> "2.00Gi\n"
(with-output-to-string (cut output-base-1000 (* 2 (expt 1000 3))))
=> "2.00G\n"
(with-output-to-string (cut output-length (* 2 (expt 1000 2))))
=> "2.00Km\n"

We also define a function to switch from one system or fixed suffix to another. We expect this function to be used interactively, so we make it return the name of the suffix or system selected as a string.

(define (output-suffix arg)
  (let ((data (assoc (string->symbol arg) *output-suffixes*)))
    (if data
      (set! output-num (cadr data))
      (let ((suffix (assoc (string->symbol arg) *suffixes*)))
        (unless suffix
          (error "Invalid type"
                 arg
                 "Expected one of:"
                 (map (compose symbol->string car)
                      (append *output-suffixes* *suffixes*))))
        (set! output-num (cut output-with-suffix suffix <>)))))
  arg)

Literal output

We also support printing the full number:

(define output-int-format-spec "~,VD~%")

(define (output-int num)
  (format #t output-int-format-spec output-digits num))

(define (output-float num)
  (format #t "~,VF~%" output-digits num))

Time intervals

We represent time intervals as numbers of seconds. We support two formats for printing them in a human-readable way: “5d03h20m12s” and “5:03:20:12”. We also allow the user to specify whether to also use weeks (when the number of days is greater than 6).

(define *show-start* 'd)
(define *show-colons* #t)

(define (output-time num)
  (cond
    ((negative? num)
     (format #t "-")
     (output-time (- num)))
    (else
      (let loop ((num num)
                 (data (find-tail (lambda (x) (eq? *show-start* (cadr x)))
                                  '((604800 w 0) (86400 d 1) (3600 h 2) (60 m 2))))
                 (force-print #f))
        (cond
          ((null? data)
           (format #t "~V,'0D~A~%"
                   (if force-print 2 0)
                   (inexact->exact (round num))
                   (if *show-colons* "" "s")))
          ((or (>= num (caar data))
               force-print)
           (format #t "~V,'0D~A"
                   (if force-print (caddar data) 0)
                   (inexact->exact (quotient num (caar data)))
                   (if *show-colons* ":" (cadar data)))
           (loop (remainder num (caar data)) (cdr data) #t))
          (else
            (loop num (cdr data) #f)))))))

Default output format

The function output-num will be used to print numbers. We set it to output-base-1024 by default:

(define output-num output-base-1024)

Program

Command line arguments

We support a few command-line arguments to specify the prefered output format.

First we create a list of supported args. The args:make-option and related functionality is provided by the args egg.

(define supported-args
  (list
    (args:make-option
      (f float)
      #:none
      "Output numbers as floats"
      (set! output-num output-float))
    (args:make-option
      (h help)
      #:none
      "Display this text"
      (usage))
    (args:make-option
      (b base)
      (#:required "BASE")
      "Set the desired base for the output"
      (set! output-num output-int)
      (cond
        ((string=? arg "16")
         (set! output-int-format-spec "~,VX~%"))
        ((string=? arg "10")
         (set! output-int-format-spec "~,VD~%"))
        ((string=? arg "8")
         (set! output-int-format-spec "~,VO~%"))
        ((string=? arg "2")
         (set! output-int-format-spec "~,VB~%"))
        (else
         (error "Invalid base (expected 2, 8, 10 or 16)" arg))))
    (args:make-option
      (s suffix)
      (#:required "TYPE")
      "Set the system of suffixes used for the output (try 'help')"
      (output-suffix arg))
    (args:make-option
      (t time)
      #:none
      "Output numbers as time intervals (specified as numbers of seconds)"
      (set! output-num output-time))
    (args:make-option
      (tn time-names)
      #:none
      "Output time with names of units instead of colons"
      (set! *show-colons* #f))
    (args:make-option
      (tw time-weeks)
      #:none
      "Output time with number of weeks"
      (set! *show-start* 'w))))

The usage function will show a header and footer around the list of supported arguments:

(define (usage)
  (format #t "Usage: ~A [OPTION ...] [FILE ...]~%~%" (car (argv)))
  (format #t (args:usage supported-args))
  (format #t "~%Report bugs to azul@freaks-unidos.net.~%")
  (format #t "Check http://wiki.freaks-unidos.net/xc for newer versions.~%")
  (exit 1))

And finally we parse all arguments. We treat operands as files, which we load:

(define (parse-args args)
  (receive (options operands)
    (args:parse args supported-args)
    (for-each load operands)))

RC File

We automatically load a “~/.xcrc” file, where the user can define, in Scheme code, functions that he wants to call from xc:

(define (load-main-rc)
  (let ((rc (make-pathname (getenv "HOME") ".xcrc")))
    (when (file-exists? rc)
      (load rc))))

Main function

(run-tests)
(parse-args (command-line-arguments))
(load-main-rc)
(stream-for-each
  (lambda (obj)
    (if (number? obj)
      (output-num obj)
      (format #t "~S~%" obj)))
  (parse-input (lexer (port->stream))))

Author

xc was created by Alejandro Forero Cuervo.

Last update: 2008-12-26 (Rev 15044)

svnwiki $Rev: 14844 $