#842 Remember unit of time in sys::Duration

qualidafial Tue 1 Dec 2009

It would be useful to store the unit of time in Duration instances.

enum DurationUnit
{
  ns,
  ms,
  sec,
  min,
  hr,
  day;

  internal Float ticksPerUnit
}

class Duration
{
  DurationUnit unit
  Float value

  once Int ticks() { value * unit.ticksPerUnit }

  Float valueIn(DurationUnit toUnit)
  {
    value * toUnit.ticksPerUnit / unit.ticksPerUnit
  }
}

This would allow clients to exploit this extra metadata about durations and put them to other uses. e.g. in my work I deal with distances and speeds:

enum DistUnit
{
  mm,
  cm,
  in,
  ft,
  m,
  yd,
  km,
  mi
}

class Dist
{
  new mm(Float value) : make(DistUnit.mm, value) {}
  new cm(Float value) : make(DistUnit.cm, value) {}
  ...
  new mile(Float miles) : make(DistUnit.mile, value) {}

  new make(DistUnit unit, Float value)
  {
    this.unit = unit
    this.value = value
  }

  DistUnit unit
  private Float value

  Float valueIn(DistUnit)

  Speed div(Duration duration)
  {
    Float speedValue = value / duration.valueIn(duration.unit)
  }
}

class SpeedUnit
{
  DistUnit dist
  DurationUnit duration
}

class Speed
{
  SpeedUnit unit
  Float value
}

Thus:

distance := Dist.mile(60)
time := 2hr
speed := distance/time

Edit: original post was submitted accidentally before I had finished.

brian Tue 1 Dec 2009

I can see that angle, although my thinking is that Duration is just a thin wrapper for a typed Int of nanoseconds and everything else just convenience.

The representation of an hour is best done with Unit.find("hour").

Currently the Duration API doesn't know anything about Unit, but if we can come up with useful methods that might make sense (I don't want to add any memory overhead to Duration though).

qualidafial Tue 1 Dec 2009

I'm still not sold on the Unit class, for a couple reasons:

  • Loss of unit-specific type safety. A unit of speed is represented by the same runtime type as a unit of length, area, volume, temperature, luminescence, etc. Good luck trying to determine what kind of unit you've got at runtime.
  • Lots of fields that are not applicable depending on the type of unit. A, K, kg, m, mol, sec. Reading the docs I still have no idea what these fields actually represent. My best guess is that they are exponents on that particular unit, e.g. with acceleration unit m/(s^2) I guess unit.m == 1 and unit.sec == -2 (?), but the docs do not explain this at all.
  • No ability to define units from other units, e.g. dividing length by time to get a unit of speed, or multiplying mass times speed to get momentum, multiplying energy by time to get work, etc. Many units are interrelated but the current interface provides no way of combining units of measurement (using multiplication or division) to get at these derived units programmatically.

brian Tue 1 Dec 2009

I'm still not sold on the Unit class

I believe that is because you want a unit based type system (or at least want to build one with the existing OO type system). That may be a valid goal, but I work with units everyday and that isn't how I've ever used units. So I don't consider a unit based type system a general purpose problem that warrants support in the core platform. Although we should strive to make it easy to build something like that as a library or DSL in Fantom.

Rather the Unit class is designed to to model a unit of measurement and provide a standard database of units and unit names. Unit does little more TimeZone - provides a standard representation and database with normalized names.

The A, K, kg, m, mol, and sec fields are what define a Unit because every unit (with the exception of weird angular stuff) is defined as ratios of these base 7 units.

The other thing the Unit class does (which crops up in internationalization) is unit conversion. Given any two Units with the same dimensions you can convert a scalar value:

fansh> f := Unit.find("fahrenheit")
fahrenheit; ┬║F; K1; 0.5555555555555556; 255.37222222222223
fansh> c := Unit.find("celsius")
celsius; ┬║C; K1; 1.0; 273.15
fansh> f.convertTo(75f, c)
23.888888888888914

So keep in mind that Unit it designed only to provide a common low level representation of units. It probably doesn't do everything you want. However, I would recommend that higher level solutions should include Unit because it is the normalized representation for a unit.

No ability to define units from other units

This could be built today by combining the ratios which are available. But to make it effective, we'd need to be able to do a reverse lookup such that things like this hold true:

meter * sec ===  Unit.find("meters_per_second")

Because of those sticky issues, I haven't tried to add those methods yet.

brian Wed 2 Dec 2009

Here is a bit more to expound upon the idea of a how higher level API should use Unit. Let's assume we don't care about memory and we want to bind every scalar with a unit (typically we'd want to efficiently handle tables/lists without carrying the unit on each overhead). That API might look like this:

const abstract class Measurement
{
  new make(Float v, Unit u)
  {
    if (u.dim != dim) throw Err("wrong dim")
    val = v
    unit = u
  }
  override Str toStr() { "$val $unit.symbol" }
  const Float val
  const Unit unit
  abstract Str dim()
}

const class Length : Measurement
{ 
  new make(Float v, Unit u) : super(v, u) {}
  override Str dim() { "m" }
}

A pragmatic rule (there are exceptions) is that a Unit dimension (ratio of 7 base units) matches the quantity being measured. So you could build your Length, Mass, etc classes but store the actual unit (m, mm, cm, km, etc).

So I think no matter what kind of higher level API one would build, the Unit class and its database would still be quite invaluable. Every solution requires this fundamental basic abstraction (which is why I decided to make it a core sys class).

jodastephen Wed 2 Dec 2009

Brian, I know you have lots of experience in this area. But I fear tat just like JSR-275, the APIs you are outlining aren't what the average developer expects or wants.

Personally, I expect either to have a separate class for each unit (one for Gram, one for Stones), or to have one class for weight that has the sclar and the unit.

Most users don't care about memory, or having special list/map formats for this. Carrying the unit in each object is an expected cost of storing the measured value correctly.

Further, I've suggested how this should integrate into Fan at the language level:

using measurements::Weight
using measurements::Distance

Void doStuff() {
  weight1 := 3_kg
  weight2 := 4.5_stone
  dist1 := 12_miles
  dist2 := 22_km
}

What is happening here is that the using statement finds a facet that declares the allowed suffixes for the units. This then allows a psuedo literal style, compiling to the same as:

using measurements::Weight
using measurements::Distance

Void doStuff() {
  weight1 := Weight(3, "kg")
  weight2 := Weight(4.5, "stone")
  dist1 := Distance(12, "miles")
  dist2 := Distance(22, "km")
}

Duration literals would be dealt with in the same way.

brian Wed 2 Dec 2009

Personally, I expect either to have a separate class for each unit (one for Gram, one for Stones), or to have one class for weight that has the sclar and the unit.

If someone wanted to build a measurement library, I would suggest the design I proposed above - a class which binds a scalar with a sys::Unit instance.

If you want to try and get crazy with the type system, you can do that too. Although if you aim for a general purpose solution, consider that Fantom's extremely incomplete database has 53 quantities and 238 units. Even if you just create a class for each quantity you are looking at a lot of classes.

But as I said before, whatever solutions get built, I think they all need a low level abstraction for Unit.

jsendra Wed 2 Dec 2009

A general-purpose solution is complex. See Frink. From Wikipedia:

"Frink is a calculating tool and programming language designed by Alan Eliasen. It is built on the Java Virtual Machine and incorporates features similar to Java, Perl, Ruby, Smalltalk, and various BASIC implementations. Its main focus is on the fields of science, engineering, physics, text processing, and education.

One of the distinguishing characteristics of Frink is that it tracks units of measure through all calculations. This allows all values to contain a quantity and its units of measure. Frink understands how different units of measure interrelate, such as a length cubed is a volume, or power multiplied by time is energy. Different units of measure can be mixed in calculations, and Frink automatically ensures that the calculations lead to a result with the expected dimensions.

height = 3 feet
gravitation = 9.80665 m/s^2
mass = 60 kg
potential_energy = height * gravitation * mass
println[potential_energy -> joules]  // Display in joules

The standard distribution comes with a data file which contains thousands of the most common units of measure, along with common data such as masses of elementary particles, planetary data, and historical measures. The standard data file uses the SI base units as its fundamental units of measure, and extends this with units for currency and information (bits). The standard data file can be completely replaced by the user, and new units and even fundamental dimensions may be added at runtime."

Login or Signup to reply.