/src/sys/java/fan/sys/Unit.java
Java | 531 lines | 385 code | 77 blank | 69 comment | 109 complexity | 387deefb97de8a6ba053813662fb8a99 MD5 | raw file
Possible License(s): CC-BY-SA-3.0
- //
- // Copyright (c) 2008, Brian Frank and Andy Frank
- // Licensed under the Academic Free License version 3.0
- //
- // History:
- // 20 Dec 08 Brian Frank Creation
- //
- package fan.sys;
- import java.util.HashMap;
- import java.util.ArrayList;
- /**
- * Unit
- */
- public final class Unit
- extends FanObj
- {
- //////////////////////////////////////////////////////////////////////////
- // Database
- //////////////////////////////////////////////////////////////////////////
- public static Unit fromStr(String name) { return fromStr(name, true); }
- public static Unit fromStr(String name, boolean checked)
- {
- synchronized (byId)
- {
- Unit unit = (Unit)byId.get(name);
- if (unit != null || !checked) return unit;
- throw Err.make("Unit not found: " + name);
- }
- }
- public static List list()
- {
- synchronized (list)
- {
- return list.dup().ro();
- }
- }
- public static List quantities()
- {
- return quantityNames;
- }
- public static List quantity(String quantity)
- {
- List list = (List)quantities.get(quantity);
- if (list == null) throw Err.make("Unknown unit database quantity: " + quantity);
- return list;
- }
- private static List loadDatabase()
- {
- InStream in = null;
- List quantityNames = new List(Sys.StrType);
- try
- {
- // open etc/sys/units.txt
- String path = "etc/sys/units.txt";
- if (Sys.isJarDist)
- in = new SysInStream(Unit.class.getClassLoader().getResourceAsStream(path));
- else
- in = Env.cur().findFile(Uri.fromStr(path)).in();
- // parse each line
- String curQuantityName = null;
- List curQuantityList = null;
- String line;
- while ((line = in.readLine()) != null)
- {
- // skip comment and blank lines
- line = line.trim();
- if (line.startsWith("//") || line.length() == 0) continue;
- // quanity sections delimited as "-- name (dim)"
- if (line.startsWith("--"))
- {
- if (curQuantityName != null) quantities.put(curQuantityName, curQuantityList.toImmutable());
- curQuantityName = line.substring(2, line.indexOf('(')).trim();
- curQuantityList = new List(Sys.UnitType);
- quantityNames.add(curQuantityName);
- continue;
- }
- // must be a unit
- try
- {
- Unit unit = Unit.define(line);
- curQuantityList.add(unit);
- }
- catch (Exception e)
- {
- System.out.println("WARNING: Init unit in etc/sys/units.txt: " + line);
- System.out.println(" " + e);
- }
- }
- quantities.put(curQuantityName, curQuantityList.toImmutable());
- }
- catch (Throwable e)
- {
- try { in.close(); } catch (Exception e2) {}
- System.out.println("WARNING: Cannot load etc/sys/units.txt");
- e.printStackTrace();
- }
- return (List)quantityNames.toImmutable();
- }
- //////////////////////////////////////////////////////////////////////////
- // Definition
- //////////////////////////////////////////////////////////////////////////
- public static Unit define(String str)
- {
- // parse
- Unit unit = null;
- try
- {
- unit = parseUnit(str);
- }
- catch (Throwable e)
- {
- String msg = str;
- if (e instanceof ParseErr) msg += ": " + ((ParseErr)e).msg();
- throw ParseErr.make("Unit", msg);
- }
- // register
- synchronized (byId)
- {
- // check that none of the units are defined
- for (int i=0; i<unit.ids.sz(); ++i)
- {
- String id = (String)unit.ids.get(i);
- if (byId.get(id) != null) throw Err.make("Unit id already defined: " + id);
- }
- // this is a new definition
- for (int i=0; i<unit.ids.sz(); ++i)
- {
- String id = (String)unit.ids.get(i);
- byId.put(id, unit);
- }
- list.add(unit);
- }
- return unit;
- }
- /**
- * Parse an un-interned unit:
- * unit := <ids> [";" <dim> [";" <scale> [";" <offset>]]]
- */
- private static Unit parseUnit(String s)
- {
- String idStrs = s;
- int c = s.indexOf(';');
- if (c > 0) idStrs = s.substring(0, c);
- List ids = FanStr.split(idStrs, Long.valueOf(','));
- if (c < 0) return new Unit(ids, dimensionless, 1, 0);
- String dim = s = s.substring(c+1).trim();
- c = s.indexOf(';');
- if (c < 0) return new Unit(ids, parseDim(dim), 1, 0);
- dim = s.substring(0, c).trim();
- String scale = s = s.substring(c+1).trim();
- c = s.indexOf(';');
- if (c < 0) return new Unit(ids, parseDim(dim), Double.parseDouble(scale), 0);
- scale = s.substring(0, c).trim();
- String offset = s.substring(c+1).trim();
- return new Unit(ids, parseDim(dim), Double.parseDouble(scale), Double.parseDouble(offset));
- }
- /**
- * Parse an dimension string and intern it:
- * dim := <ratio> ["*" <ratio>]*
- * ratio := <base> <exp>
- * base := "kg" | "m" | "sec" | "K" | "A" | "mol" | "cd"
- */
- private static Dimension parseDim(String s)
- {
- // handle empty string as dimensionless
- if (s.length() == 0) return dimensionless;
- // parse dimension
- Dimension dim = new Dimension();
- List ratios = FanStr.split(s, (long)'*', true);
- for (int i=0; i<ratios.sz(); ++i)
- {
- String r = (String)ratios.get(i);
- if (r.startsWith("kg")) { dim.kg = Byte.parseByte(r.substring(2).trim()); continue; }
- if (r.startsWith("sec")) { dim.sec = Byte.parseByte(r.substring(3).trim()); continue; }
- if (r.startsWith("mol")) { dim.mol = Byte.parseByte(r.substring(3).trim()); continue; }
- if (r.startsWith("m")) { dim.m = Byte.parseByte(r.substring(1).trim()); continue; }
- if (r.startsWith("K")) { dim.K = Byte.parseByte(r.substring(1).trim()); continue; }
- if (r.startsWith("A")) { dim.A = Byte.parseByte(r.substring(1).trim()); continue; }
- if (r.startsWith("cd")) { dim.cd = Byte.parseByte(r.substring(2).trim()); continue; }
- throw ParseErr.make("Bad ratio '" + r + "'");
- }
- // intern
- return dim.intern();
- }
- /**
- * Private constructor.
- */
- private Unit(List ids, Dimension dim, double scale, double offset)
- {
- this.ids = checkIds(ids);
- this.dim = dim;
- this.scale = scale;
- this.offset = offset;
- }
- static List checkIds(List ids)
- {
- if (ids.sz() == 0) throw ParseErr.make("No unit ids defined");
- for (int i=0; i<ids.sz(); ++i) checkId((String)ids.get(i));
- return (List)ids.toImmutable();
- }
- static void checkId(String id)
- {
- if (id.length() == 0) throw ParseErr.make("Invalid unit id length 0");
- for (int i=0; i<id.length(); ++i)
- {
- int c = id.charAt(i);
- if (FanInt.isAlpha(c) || c == '_' || c == '%' || c == '/' || c == '$' || c > 128) continue;
- throw ParseErr.make("Invalid unit id " + id + " (invalid char '" + (char)c + "')");
- }
- }
- //////////////////////////////////////////////////////////////////////////
- // Identity
- //////////////////////////////////////////////////////////////////////////
- public final boolean equals(Object obj) { return this == obj; }
- public final int hashCode() { return toStr().hashCode(); }
- public final long hash() { return FanObj.hash(toStr()); }
- public final Type typeof() { return Sys.UnitType; }
- public final String toStr() { return (String)ids.last(); }
- public final List ids() { return ids; }
- public final String name() { return (String)ids.first(); }
- public final String symbol() { return (String)ids.last(); }
- public final double scale() { return scale; }
- public final double offset() { return offset; }
- public final String definition()
- {
- StringBuilder s = new StringBuilder();
- for (int i=0; i<ids.sz(); ++i)
- {
- if (i > 0) s.append(", ");
- s.append(ids.get(i));
- }
- if (dim != dimensionless)
- {
- s.append("; ").append(dim);
- if (scale != 1.0 || offset != 0.0)
- {
- s.append("; ").append(scale);
- if (offset != 0.0) s.append("; ").append(offset);
- }
- }
- return s.toString();
- }
- //////////////////////////////////////////////////////////////////////////
- // Dimension
- //////////////////////////////////////////////////////////////////////////
- public final long kg() { return dim.kg; }
- public final long m() { return dim.m; }
- public final long sec() { return dim.sec; }
- public final long K() { return dim.K; }
- public final long A() { return dim.A; }
- public final long mol() { return dim.mol; }
- public final long cd() { return dim.cd; }
- static class Dimension
- {
- public int hashCode()
- {
- return (kg << 28) ^ (m << 23) ^ (sec << 18) ^
- (K << 13) ^ (A << 8) ^ (mol << 3) ^ cd;
- }
- public boolean equals(Object o)
- {
- Dimension x = (Dimension)o;
- return kg == x.kg && m == x.m && sec == x.sec && K == x.K &&
- A == x.A && mol == x.mol && cd == x.cd;
- }
- public String toString()
- {
- if (str == null)
- {
- StringBuilder s = new StringBuilder();
- append(s, "kg", kg); append(s, "m", m);
- append(s, "sec", sec); append(s, "K", K);
- append(s, "A", A); append(s, "mol", mol);
- append(s, "cd", cd);
- str = s.toString();
- }
- return str;
- }
- private void append(StringBuilder s, String key, int val)
- {
- if (val == 0) return;
- if (s.length() > 0) s.append('*');
- s.append(key).append(val);
- }
- public Dimension add(Dimension b)
- {
- Dimension r = new Dimension();
- r.kg = (byte)(kg + b.kg);
- r.m = (byte)(m + b.m);
- r.sec = (byte)(sec + b.sec);
- r.K = (byte)(K + b.K);
- r.A = (byte)(A + b.A);
- r.mol = (byte)(mol + b.mol);
- r.cd = (byte)(cd + b.cd);
- return r;
- }
- public Dimension subtract(Dimension b)
- {
- Dimension r = new Dimension();
- r.kg = (byte)(kg - b.kg);
- r.m = (byte)(m - b.m);
- r.sec = (byte)(sec - b.sec);
- r.K = (byte)(K - b.K);
- r.A = (byte)(A - b.A);
- r.mol = (byte)(mol - b.mol);
- r.cd = (byte)(cd - b.cd);
- return r;
- }
- public Dimension intern()
- {
- // intern
- synchronized (dims)
- {
- Dimension cached = (Dimension)dims.get(this);
- if (cached != null) return cached;
- dims.put(this, this);
- return this;
- }
- }
- public boolean isDimensionless()
- {
- return toString().length() == 0;
- }
- String str;
- byte kg, m, sec, K, A, mol, cd;
- }
- //////////////////////////////////////////////////////////////////////////
- // Arithmetic
- //////////////////////////////////////////////////////////////////////////
- public final Unit mult(Unit b)
- {
- synchronized (combos)
- {
- Combo key = new Combo(this, "*", b);
- Unit r = (Unit)combos.get(key);
- if (r == null)
- {
- r = findMult(this, b);
- combos.put(key, r);
- }
- return r;
- }
- }
- private static Unit findMult(Unit a, Unit b)
- {
- // if either is dimensionless give up immediately
- if (a.dim.isDimensionless() || b.dim.isDimensionless())
- throw Err.make("Cannot compute dimensionless: " + a + " * " + b);
- // compute dim/scale of a * b
- Dimension dim = a.dim.add(b.dim).intern();
- double scale = a.scale * b.scale;
- // find all the matches
- Unit[] matches = match(dim, scale);
- if (matches.length == 1) return matches[0];
- // right how our technique for resolving multiple matches is lame
- String expectedName = a.name() + "_" + b.name();
- for (int i=0; i<matches.length; ++i)
- if (matches[i].name().equals(expectedName))
- return matches[i];
- // for now just give up
- throw Err.make("Cannot match to db: " + a + " * " + b);
- }
- public final Unit div(Unit b)
- {
- synchronized (combos)
- {
- Combo key = new Combo(this, "/", b);
- Unit r = (Unit)combos.get(key);
- if (r == null)
- {
- r = findDiv(this, b);
- combos.put(key, r);
- }
- return r;
- }
- }
- public final Unit findDiv(Unit a, Unit b)
- {
- // if either is dimensionless give up immediately
- if (a.dim.isDimensionless() || b.dim.isDimensionless())
- throw Err.make("Cannot compute dimensionless: " + a + " / " + b);
- // compute dim/scale of a / b
- Dimension dim = a.dim.subtract(b.dim).intern();
- double scale = a.scale / b.scale;
- // find all the matches
- Unit[] matches = match(dim, scale);
- if (matches.length == 1) return matches[0];
- // right how our technique for resolving multiple matches is lame
- String expectedName = a.name() + "_per_" + b.name();
- for (int i=0; i<matches.length; ++i)
- if (matches[i].name().contains(expectedName))
- return matches[i];
- // for now just give up
- throw Err.make("Cannot match to db: " + a + " / " + b);
- }
- private static Unit[] match(Dimension dim, double scale)
- {
- ArrayList acc = new ArrayList();
- synchronized (list)
- {
- for (int i=0; i<list.sz(); ++i)
- {
- Unit x = (Unit)list.get(i);
- if (x.dim == dim && approx(x.scale, scale))
- acc.add(x);
- }
- }
- return (Unit[])acc.toArray(new Unit[acc.size()]);
- }
- private static boolean approx(double a, double b)
- {
- // pretty loose with our approximation because the database
- // doesn't have super great resolution for some normalizations
- if (a == b) return true;
- double t = Math.min( Math.abs(a/1e3), Math.abs(b/1e3) );
- return Math.abs(a - b) <= t;
- }
- static class Combo
- {
- Combo(Unit a, String op, Unit b) { this.a = a; this.op = op; this.b = b; }
- public int hashCode() { return a.hashCode() ^ op.hashCode() ^ (b.hashCode() << 13); }
- public boolean equals(Object that) { Combo x = (Combo)that; return a == x.a && op == x.op && b == x.b; }
- final Unit a;
- final String op;
- final Unit b;
- }
- //////////////////////////////////////////////////////////////////////////
- // Conversion
- //////////////////////////////////////////////////////////////////////////
- public final double convertTo(double scalar, Unit to)
- {
- if (dim != to.dim) throw Err.make("Incovertable units: " + this + " and " + to);
- return ((scalar * this.scale + this.offset) - to.offset) / to.scale;
- }
- //////////////////////////////////////////////////////////////////////////
- // Fields
- //////////////////////////////////////////////////////////////////////////
- private static final List list = new List(Sys.UnitType);
- private static final HashMap byId = new HashMap(); // String id-> Unit
- private static final HashMap dims = new HashMap(); // Dimension -> Dimension
- private static final HashMap quantities = new HashMap(); // String -> List
- private static final HashMap combos = new HashMap(); // Combo -> Unit
- private static final List quantityNames;
- private static final Dimension dimensionless = new Dimension();
- static
- {
- dims.put(dimensionless, dimensionless);
- quantityNames = loadDatabase();
- }
- private final List ids;
- private final double scale;
- private final double offset;
- private final Dimension dim;
- }