The MEASURES-Package ==================== Roman Cunis MAZ GmbH, Karnapp 20, D-2100 Hamburg 90, Germany The Measures Package provides dimensioned numbers (i.e. numbers with dimensional units, short: dim-numbers) for CommonLisp and CLOS. Calculations can be done with dim-numbers in the same way as with normal numbers, e.g.: (dim+ 12m 34cm) -> 12.34m (dim* 3m 3m) -> 9qm The documentation is structured as follows: 1. Notation for units of measurements and dim-numbers. 2. Definition of units of measurement and relations between units. 3. Input support of dim-numbers. 4. Operations on dim-numbers. 5. Output formats for defined units. 6. Miscellaneous features All definitions are contained in the package 'measures' (nickname is 'ms'). The code runs as well under plain CommonLisp as under CLOS. 1. Notation for Units of Measurement and Dim-Numbers ---------------------------------------------------- A simple unit of measurement is written as a sequence of alphabetic chars where case is significant: e.g. m (meter), s (second), ms (millisecond), mph (miles per hour). (Special characters of your character set might additionally be declared using define-unit-char (see Section 6), #\$ is already predefined.) More complex units of measurement can be written as multiplicative expressions of simple units. In general, an expression 2 3 a b ------- , where a,b,c,d are some units, 2,3 may be any positive 2 3 exponents, c d is written as "a2b3/c2d3". An exponent of 1 can be written as 1, can be substituted by '.' between units and may be omitted at the end of nominator or denominator in a unit expression. Thus, the following are equivalent: "m1s2" == "m.s2" == "s2m" Valid unit expressions are e.g.: "m2" (square meter), "km/h" (kilometer per hour), "kg.m/s2" (kilogram meter per square second, i.e. 1 Newton). Unit expressions constructed from defined simple units may be used for input of dim-numbers and will be automatically be generated for output of results: E.g. (dim* 9m 5m2) ==> 45m3 is valid even if only "m" is defined and "m2", "m3" have never been declared explicitly. [NOTE: As clearly to be seen, unit handling and conversion is done by multiplicative means. Therefore, conversions that need a constant offset are not supported: e.g. F(ahrenheit) can NOT be expressed in terms of C(elsius) or vice versa.] Dim-numbers consist of a number (any valid number representation is allowed) immediately followed by a unit expression, e.g.: 5m -5.3s 3/2m.s 3.5e-2ms [Note, that the single letter "e" must never be defined as unit in order to allow exponential notation for input.] 2. Definition of Units of Measurement ------------------------------------- Each measurement, e.g. distance, area, time, etc., requires the definition of a *base unit*. Scaled units can be defined relative to base units. Measurements can be derived from other measurements, e.g. area from distance. Those relationships will be taken into account when calculating with dim-numbers. Non-derived measurements are called *primitive* measurements. The definition of units of measurement is done by means of the macro defmeasure: (defmeasure | () :units ( * [:metric | :scientific] ) :format ) is the name of the measurement, e.g. distance, area, time. is the base-unit defined for that measure, e.g. "m" for distance, "s" for time. Derived measurements may be defined using the corresponding unit-expression, e.g. "m2" for area. If the base-unit for a derived measurement requires a spelling different from the corresponding unit-expression, a base-unit definition might be given as association of a unit-string with a unit-expression, e.g. ("mps" "m/s") for speed, ("qm" "m2") for area. [Note however, that *only* base-units may be used in the definition if new base-units. E.g. "mph" cannot be given as base-unit for speed if "m" and "s" are base-units for distance and time.] Relative units to the base-units are given as a list of following :units. A is either a unit-expression consisting of known units, or a list beginning with a new simple unit-string, followed by scaling factors and unit-expressions: ( * +). E.g.: ("cm" 1/100 "m") ("min" 60 "s") [Note however, that dim-numbers may not be used in the definition of units: WRONG: ("min" "60s").] If a whole range of metric units is required for a measurement, exactly one of the keywords :scientific or :metric can be given within the unit-definition list: :scientific generates the prefixes "p-" (pico-, 10e-12), "n-" (nano-, 10e-9), "u-" (micro-, 10e-6, "u" is used for lack of the greek letter mu), "m-" (milli-, 10e-3), "k-" (kilo-, 10e3), "M-" (Mega-, 10e6), "G-" (Giga-, 10e9). E.g.: (defmeasure capacity "F" :units (:scientific)) would generate "pF", "nF", "uF", "mF", "F", "kF", "MF", "GF". :metric generates commonly used European prefixes: "m-" (milli, 1/1000), "c-" (centi-, 1/100), "d-" (deci-, 1/10), "k-" (kilo-, 1000). E.g.: (defmeasure distance "m" :units :metric) generates "mm", "cm", "dm", "m", "km". [Note, that if only :scientific or :metric is given to :units, the symbol may replace the list.] specifies an optional permanent output format for the measurement; for details see section 5. All unit-strings or unit-expressions can be given either as strings or as symbols (remember to use the vertical-bar notation in order to preserve case), e.g. "m" == |m|. All units are internally stored as symbols containing a reference to its measurement on its property-list. Strings will be internalized in the current package. Take care not to switch packages between the definition and use of units (or else explicitly export unit symbols). Some complete examples: (defmeasure distance "m" :units (:metric ("in" 0.0254 "m") ("ft" 12 "in") (|yd| 3 |ft|)) ) (defmeasure area "m2" :units (("sqyd" "yd" "yd") ("acre" 4840 |sqyd|)) ) (defmeasure time "s" :units (("min" 60 "s") ("h" 60 "min") ("day" 24 "h")) ) (defmeasure speed "m/s" :units ("km/h" ("mph" 1.6 "km/h")) ) For every defined measurement a description object is generated (a struct of type 'measure' or --with CLOS-- an instance of class 'measure'). This object contains all relevant information about a measure. The function pprint-measure (see section 6) can be used to show this information. E.g.: (pprint-measure 'distance) => {{ Base-Unit: |m| Scale: ((|km| . 1000) (\m . 1) (|yd| . 1143/1250) (|ft| . 381/1250) (|dm| . 1/10) (|in| . 127/5000) (|cm| . 1/100) (|mm| . 1/1000)) Prim-Id: 2 Output-Format: }} 3. Input of Dim-Numbers ----------------------- Intuitively one would want to input dim-numbers just like any other number within the listener or in response to read-functions. In order to achieve this, a read-macro can be set up so that all tokens beginning with a digit or as sign (#\+, #\-) are first scanned whether they represent a dim-number. If this is the case, the appropriate dim-number object is created, otherwise the normal reading process continues, thus resulting in a normal number representation or a symbol if no conversion is possible. This read-macro must explicitly be set up in your environment by calling (install-dim-number-reader :permanent t) [Alternatively you might set your *readtable* to the package's internal ms::*dim-number-readtable* to temporarily activate the read-macro.] If you want to have a read-macro but want to avoid overloading common characters like digits, you might specify a dispatch-macro character instead: (install-dim-number-reader :dispatch #\M) enables input of dim-numbers in the form #M12.3m . Explicit control about dim-number parsing is provided by the functions - read-dim-number &optional (stream *standard-input*) - parse-dim-number string &key (:start 0) (:end (length string)) read-dim-number reads a dim-number from stream analogously to read. parse-dim-number parses a dim-number from a string (analogously to read-from-string). Both functions return the dim-number read, or else whatever read or read-from-string would have returned. parse-dim-number returns the index of the first char after the parsed dim-number as second value. If necessary, the function dim-number may be used to explicitly create a dim-number from a given value and unit: dim-number Both arguments are evaluated. may be given as either symbol or string. E.g.: (dim-number 3.2 '|m|) => 3.2m 4. Operations on Dim-Numbers ---------------------------- A whole suite of numerical operations is provided for dim-numbers. Note, that all operations can handle normal numbers as well. dim-value &optional returns the value part of a dim-number. Without this value corresponds to the measurement's base-unit (this is the value with which the dim-number is internally represented). If is given, the value will be converted accordingly. Both arguments are evaluated. may be given as either symbol or string. E.g.: (dim-value 123mm) => 0.123 , (dim-value 123mm "cm") => 12.3 [If is a normal number, dim-value returns that number.] dim+, dim-, dim-max, dim-min are defined analogously to +, -, max, min, resp. All require, that all operands have compatible units. Otherwise an error is signalled. (If you can guarantee that all dim-number operands are compatible, you might use sdim+, sdim-, sdim-max, sdim-min ('s-' for 'safe') instead that do not perform the compatibility check.) dim*, dim/ are defined analogously to *, /. Dim-numbers and normal operands are both allowed as operands. These functions return dim-numbers as a result unless the units are factored out in the course of the operation. In this case the appropriate number is returned. E.g.: (dim/ 6m 300mm) => 20 dim-expt, dim-sqrt are defined analogously to expt, sqrt. Note that dim-sqrt and dim-expt with a negative power will return an error, if the unit of the operand cannot be reduced appropriately: E.g. (dim-sqrt 9m2) => 3m ; (dim-sqrt 10s) => ERROR! dim=, dim/=, dim<, dim<=, dim>, dim>= are defined analogously to the common comparison operators. All require, that all operands have compatible units. Otherwise an error is signalled. (If you can guarantee that all dim-number operands are compatible, you might use sdim=, sdim/=, etc. ('s-' for 'safe') instead that do not perform the compatibility check.) dim-zerop returns T for all 0 values independent of unit. It will return T for the number 0 as well. 5. Output Formatting for Dim-Numbers ------------------------------------ Dim-numbers are internally stored as structs of type dim-number. The print-function of these structs is set so that output is formatted correctly. (Normally this is completely transparent to the user. However, if for some reasons output formatting should be suppressed, the internal global variable ms::*print-formatted-dim* may be set to NIL.) Output formatting can be controlled for each measurement individually. Thus you may control which unit of measurement is used for a dim-number of a certain measurement. There are five output modes available: :base -- always use the base-unit. :current -- use the unit with which the dim-number has been input or generated. If this cannot be determined (e.g. after numerical operations) use the base-unit. (This is the default for all measurements.) :unit -- use the specified unit. :best-fit -- use that unit that requires the minimum number of digits before the decimal point, or the minimum number of zeroes after it (see example below). :step ... -- stepwise decomposition into integer values according to the specified units. All output formats may optionally be followed by a single positive integer that --if applicable-- controls the output precision for float values. The following examples illustrate all formats for "17.42h" (hours): :current 17.42h :current 1 17.4h :base 62712s :best-fit 0.726days :best-fit 2 0.73days :step "h" "min" "s" 17h:25min:12s [NOTE that the output format for :step can NOT be used for input!] Here is a detailed example for :best-fit : Using :best-fit for 'distance' the following values will be printed as shown in the right column: 0.0001m -> 0.1mm 0.001m -> 1mm 0.01m -> 1cm 0.1m -> 0.1m 1m -> 1m 10m -> 10m 100m -> 0.1km 1000m -> 1km In determining the best fit for :best-fit, all defined units of a measurement are taken into account. This might result in unexpected choices if non-metric units are present (like "in" or "ft"). In order to exclude such units, specify all *admissible* units explicitly with :best-fit, e.g. :best-fit "mm" "cm" "m" "km". For the :step-format, the global variables *step-skip-leading-zeros* and *step-skip-trailing-zeros* control whether leading or trailing units, resp., will be shown if their value is 0. Both variables are initially set to T. E.g. with (:step "days" "h" "min" "s"), "75min" are by default printed as "1h:15min". This would become "0days:1h:15min:0s" if both variables were NIL. All macros and functions that control output formatting accept a format description list. A single keyword symbol is sufficient, if the list would contain only one element. The output format can be permanently specified for a given measurement using the :format parameter of defmeasure. E.g.: (defmeasure distance "m" :units ... :format (:current 3)) The format may be permanently changed by setf-ing the access function ms-output-format of a measure object. E.g.: (setf (ms-output-format (measure-named "distance")) '(:best-fit "mm" "m" "km")) The format may be temporarily changed for one or more measures using the macro with-unit-format: (with-unit-format (([] ) ...) ... ) E.g.: (with-unit-format ((distance :base) (time :step "h" "min" "s" 2)) ... ) If only one format is given, the outermost brackets may be omitted. If no is given, the specified format applies to *all* measures. E.g. (with-unit-format (:base) ...) is the suggested environment for printing values to a file in order to guarantee re-readability. (Remember that the :step-format would not be readable!) A single dim-number may be printed with a specified format using the macro print-converted. print-converted &rest Only the is evaluated. E.g.: (print-converted 17.24h :unit "min" 2) 6. Miscellaneous Functions -------------------------- Here is a set of additional functions supporting the handling of measure objects and dim-numbers. dim-number-p returns T if is an object of type dim-number (or a subtype thereof). dim-measure returns the measure object corresponding to . measuring-p returns T if belongs to (given as object or symbol). measure-p tests whether is a measurement description object. measure-named returns --if present-- the measure object for the measurement named (given as symbol). Otherwise it returns NIL. ms-base-unit, ms-scale, ms-name access attributes of measure objects. None of these should be used for modification. ms-output-format accesses the permanent output format of a measure object. With setf it may be used to change the permanent output format (see section 5). pprint-measure pretty-prints the information contained in a measurement description object as shown in section 2. may either be a measure object or the name of one (given as symbol). delete-measure deletes a measure object (given as object or name symbol) and all units defined for it. Moreover, if is primitive, all derived measures will ALSO be deleted! (E.g. deleting 'distance' would in turn delete 'area' and 'speed', too.) As this is a potentially desastrous operation (if there are still dim-numbers for those measures around), y-or-n-queries will be output in order to confirm the deletion. delete-unit deletes (given as string or symbol) from its measurement. However, if names a base-unit, the corresponding measurement is deleted as with delete-measure. get-derived-measures returns a list of all measure objects that are derived from (given as measure object). E.g.: (get-derived-measures (measure-named 'distance)) => ( # # ) declare-unit-char declares to be a valid constituent of simple unit strings (see section 1). This allows the use of special characters, e.g. greek characters as mu or delta, or special currency symbols (e.g. Cent, Pound Sterling, Yen), to be used as or included in units. *measures-version* is a global variable containing version information about the measures package. A. Appendix for CLTL2 Users --------------------------- Two enhancements specified in CLTL2 are of great use for the measurement package. Unfortunately, at the time at which the package was written, not all CLTL2-compliant Lisp systems implemented those features correctly. Therefore the package expects the feature keyword :CLTL2X to be explicitly added to your *features* list in order to assert the availability of these features. The first feature to be used is (setf (readtable-case ) :preserve) This allows the dim-number-reader to use the normal token reading features of CommonLisp, even where case should be preserved. This is more efficient than the technique of reading a token character by character, that has to be used in CLTL1 and incomplete implementations of CLTL2. The second feature is concerned with storing and loading of dim-numbers. The normal storage mechanisms of CommonLisp would store a struct with all its attributes. Dim-numbers will thus be stored with their value and their internal measurement id (prim-id). As prim-ids are assigned to measure objects dynamically dependent on the sequence of declaration, re-reading dim-numbers from file will only work, if all measurements have been assigned the same prim-ids as when stored. However, CLTL2 and CLOS provide the possibility of specifying a load-form for objects of a specific type. With :CLTL2X given, the measures package defines such a load-form for dim-numbers so that dim-numbers will be stored with their value and the unit-symbol of their base-unit. This will allow stable re-loading independent of internally assigned prim-ids.