Last published: December 12, 2008 by 'ken'
Defines 16 Classes
Extends 22 Classes
Author: Travis Griggs
This Parcel adds a unitized number system to VisualWorks (5i and above). It is a cleanroom implementation of a similiar system put together a number of years back by Ken Greene. I thought a FAQ would be the best way to describe what it does and how it does it.
Q: How is this thing Licenced?
A: The Licence is basically Public Domain with one caveat. Keep this comment (or one that gives appropriate credit) in place with the code. Other than that, take it, sell it, plunder it, whatever, just make sure that in particular Ken Greene gets credit for the rather nifty units reduction trick.
Q: That behind us, what does it do?
A: It allows you to create measurement objects which are composed of a magnitude and a unit of measure. These objects support arithmetic. Units are automatically converted when necessary. A given measurment object can be converted to another unit scale.
Q: How do I create one?
A: There's a binary selector that can be sent with any ArithmeticValue as its reciever. The argument is a string representing the unit. For example:
4 ~> 's' returns four seconds
-1 ~> 'K' returns negative one Kelvin
4.2 ~> 'ft' returns 4.2 feet
I struggled with the binary selector. In playing with them in workspaces, it became obvious that a binary selector made working with them a lot easier. In the end I came down to => or ~>. I chose the second because it made me think "a different sort of association", which is really all these things are. If you've got a better idea, I'm open to suggestions. You can also create and read units from a string, for example (Measurement readFromString: '2 km').
Q: How do I convert a measurement?
A: Send a measurement the ~> message with the desired unitString as it's argument. For example:
12.0 ~> 'mm' ~> 'cm' will return 1.2 centimeters.
Q: What about complex units?
A: The unit string can express a complex unit. A dot character is used to represent unit multiplication. Eliot Miranda will call me a hypocrite for doing this (he should). It seemed the closest thing to the traditional midline product dot. Division is done via the / character. Some examples:
(Unit of: 'm/s') will return a meters per second unit
(Unit of: 'N.m') will return a Newton-meter, the common SI unit of torque
(Unit of: 'ft2') will return square feet
(Unit of: 'kg.m2/s3.A2') will return a kilogram times meter square per second cubed and Ampere squared. This is also known as an ohm. :)
(Unit of: 'kg.m/m2.s/s') will return a kilogram per meter (note the automatic reduction)
Q: What does the base system have defined for units?
A: Quite a few actually. The Unit class maintains a database of units. As it is forced to create new ones (either at your behest, or automatically thru derived complex units), it adds them to the database. You can send the message allUnits to the Unit class. Another good place to look is at the various class side install* messages in BaseUnit and DerivedUnit. You'll find all kinds of good stuff in there, like candela and mole and Faraday and furlong and cubit and township, plus the ones you probably were interested in. If you want to install some units of your own, I would suggest adding your own install* method to DerivedUnit class. If you're adding units that are not terribly particular to your domain, I'd love to get access to them so that I can consider them for inclusion in the base.
Q: What about SI prefixes, milli, micro, etc?
A: There are a number of defined SI prefixes. While one could apply milli to an inches, I did not. That doesn't mean that you can't add a unit called milli-inch, or whatever, I just didn't in the base. I chose an arbitrary set of base and derived SI units and applied all of the known prefixes to them.
Q: What about mixed mode arithmetic? Can I multiply a unit by a simple scalar?
A: Yes. And vice versa. There is a special unit called "unitless". This is used whenever units in the denominator and those in the numerator completely cancel each other out. This represents the un-unit'ed number system.
Q: Points too?
A: Points are symmetric, but not. Consider the following two cases:
(2 ~> 'ft') * (3@7) yields (14 @ 21) ~> 'ft'.
(3@7) * (2 ~> 'ft') yeilds ((14 ~> 'ft')@(21 ~> 'f't))
They are for the most part equivalent, but they will fail the = method. This should probably be fixed. If you have the NumericCollections parcel loaded, it exhibits the same "behavior". In either case you run into the "aggregate of magnitudes associated with a unit versus aggregate of magnitude/unit associations" situation.
Q: How does it do the reduction?
A: IOW, why is this cool? It takes advantage of Smalltalk's higher-level math facilities (namely, unbounded Integers and Fractions). Each unit in the system is assigned a unique key. If it is a something per something unit, it will be a Fraction, otherwise, it will be an Integer. Now consider what happens if you assign each of the basic units an integer key WHICH IS PRIME. Let's do an example. Meters gets the first prime, 2. Seconds gets the second prime, 3. Now to get the units meter-seconds, one just multiplies 2 by 3, yielding 6. To get meters per seconds, the unique key becomes 2/3. Smalltalks Integer/Fraction math mechanism takes care of the rest. If you multiply a unit of meters per second times a unit of seconds, it turns into 2/3 times 3, which reduces automatically 2, which represents meters. The seconds dropped out. If you had multiplied meters per second (2/3) by seconds per meter (3/2), you'd get 1, which by definition is the unit key for the unitless value. Pretty cool, huh? My hat is off to Ken Greene for seeing this years ago.
Q: What about temperatures, aren't they tricky because they require an offset to convert?
A: If we were to do that, yes. But we sidesteped the issue; or rather, we keep the system consistent. Temperature measurements can either be absolute or deltas. The measurement system deals with measurement deltas. IOW, a 10 °F temperture increase can be directly converted to the equivalent °C temperature increment with a simple scalar; the offset is irrelevant. So when dealing with temperatures in the Measurements system, they are treated as deltas. However, it is more common to deal with Temperatures as absolute values. Note that the same discussion can be had about units of Time. The Measurements system treats 12 hours as a 12 hour delta, NOT as high noon. So, in the spirit of the relative Time objects (Time, Date, and Timestamp), a Temperature object was created. You can specify them with a string such as Temperature readFromString: '100 °C'. Both Temperatures and the base VisualWorks time objects have a limited interoperability with the measurements system. Consider the following examples:
(Temperature readFromString: '100 °C') - (10 ~> 'K') = (Temperature readFromString: '90 °C'). "A relative minus a delta gives another relative"
(Timestamp readFromString: '9/9/1999') - (Timestamp readFromString: '8/8/1888') = (40573 ~> 'day'). "A relative minus a relative gives a delta"
(Time readFromString: '18:00') + (1 ~> 'hr') = (Time readFromString: '19:00') "A relative plus a delta gives a relative"
These operations are (at this time) one directional, the relative object must be the reciever. You can convert Temperatures from one unit to another by either sending as: aUnitString to a Temperature, or sending it a specific asKelvin, etc, message.
Q: How does one get that little degree symbol?
A: In VisualWorks, it's a composed character. Type the following sequence (delimeted by the angle brackets): <^><0>.
Q: Is this a finished version?
A: No, this is a first cut. I wanted to get it out in the public; I'm a firm believer that that process is better for code health.
A: What areas can you see changing/improving then?
Q: A couple.
1) It is possible to make the relative<->delta operations bi directional. New base class types are created for Time and Temperature. BaseUnit classes indicate the DerivedUnit type (original, Time, or Temperature based) to create. Some of the math code moves over to the Unit object. This then gives a localized place to deal with the interoperability stuff. However, that's a bit more complex, and I'm not sure it's worth it.
2) More UI support. An input field that can interface units of measure with the user would be handy. Whether to do a separate Spec class, or modify/extend the existing InputField Spec and add additional PrintConverter stuff would need to be played with.
3) More units. I would also like to get a good list of definitive unit abbreviations. I guessed on some of these things.
4) Locale support. VW locales support collation, currency, etc. It would be nice if they could indicate the localized standard for printing various units of measure (default for liquid volume for example).
5) The KeyOnlyUnit is basically an optimization to keep things going quick under multiply/divide conditions. It just wraps Integers/Fractions. It would be nice to experiment with just using those objects directly, by moving the KeyOnlyUnit "wrapper" behavior straight to Integer/Fraction as extensions. This would save extra method dispatches as well as reduce allocation overhead under heavy computations.