All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.squarespace.cldrengine.units.conversion.UnitFactors Maven / Gradle / Ivy

The newest version!
package com.squarespace.cldrengine.units.conversion;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import com.squarespace.cldrengine.api.Decimal;
import com.squarespace.cldrengine.api.Rational;
import com.squarespace.cldrengine.api.UnitType;
import com.squarespace.cldrengine.utils.Heap;

import lombok.AllArgsConstructor;
import lombok.ToString;

/**
 * Build a graph of conversion factors between units. Sets of conversion
 * factors are closed, e.g. any factor must be convertable into any other
 * unit in the set. Their must be a connected path of length 1 or greater
 * between any two units.
 *
 * Gaps in the graph represent conversion factors that are unknown at the
 * time of graph construction. These are incrementally filled in on demand
 * by finding the shortest and lowest-cost conversion path between two units
 * with no direct conversion factor. The path is transformed into a direct
 * conversion factor by multiplying all factors along the path. Finally the
 * new factor is added to the graph.
 */
public class UnitFactors {

  private static final Rational ONE = new Rational("1");

  public final Set unitset = new HashSet<>();
  public final List units;
  private final List factors;
  private final Map> graph = new EnumMap<>(UnitType.class);
  private final ConcurrentMap> cache =
      new ConcurrentHashMap<>();

  public UnitFactors(List factors) {
    this(factors, false);
  }

  /**
   * Build a unit converter graph using the given factors.
   */
  public UnitFactors(List factors, boolean memoize) {
    this.factors = factors;
    int size = factors.size();
    for (int i = 0; i < size; i++) {
      FactorDef factor = factors.get(i);
      this.unitset.add(factor.src);
      this.unitset.add(factor.dst);
    }
    this.units = new ArrayList<>(this.unitset);
    this.units.sort((a, b) -> a.value().compareTo(b.value()));
    this.init();
  }

  @Override
  public String toString() {
    StringBuilder buf = new StringBuilder();
    for (UnitType src : graph.keySet()) {
      Map m = graph.get(src);
      buf.append(src.value()).append(":\n");
      if (m != null) {
        for (UnitType dst : m.keySet()) {
          buf.append("  ").append(dst.value()).append(" = ").append(m.get(dst)).append('\n');
        }
      }
    }
    return buf.toString();
  }

  /**
   * Return the factor that converts units of 'src' into 'dst'.
   */
  public UnitConversion get(UnitType src, UnitType dst) {
    // Units are the same, conversion is 1
    if (src == dst) {
      return new UnitConversion(Arrays.asList(src, dst), Arrays.asList(ONE));
    }

    // See if a direct conversion exists.
    Rational fac = this.graph.get(src).get(dst);
    if (fac != null) {
      return new UnitConversion(Arrays.asList(src, dst), Arrays.asList(fac));
    }

    // Find the shortest, lowest-cost conversion path between
    // the two factors
    List path = this.shortestPath(src, dst);
    if (path != null) {
      // Multiply the factors together
      List factors = new ArrayList<>();
      UnitType curr = path.get(0);
      for (int i = 1; i < path.size(); i++) {
        UnitType next = path.get(i);
        Rational nextfac = this.graph.get(curr).get(next);
        factors.add(nextfac);
        curr = next;
      }

      // Cache this conversion path
      ConcurrentMap m = this.cache.computeIfAbsent(src, s -> new ConcurrentHashMap<>());
      return m.computeIfAbsent(dst, s -> new UnitConversion(path, factors));
    }

    // No conversion factor exists
    return null;
  }

  /**
   * Constructing the initial factor graph.
   */
  private void init() {
    for (FactorDef factor : this.factors) {
      UnitType src = factor.src;
      UnitType dst = factor.dst;
      Rational rat = new Rational(factor.factor);

      // Convert src -> dst
      Map m = this.graph.get(src);
      if (m == null) {
        m = new EnumMap<>(UnitType.class);
        this.graph.put(src, m);
      }
      m.put(dst, rat);

      // Convert dst -> src, if an explicit mapping does not already exist
      m = this.graph.get(dst);
      if (m == null) {
        m = new EnumMap<>(UnitType.class);
        this.graph.put(dst, m);
      }
      Rational tmp = m.get(src);
      if (tmp == null) {
        m.put(src, rat.inverse());
      }
    }
  }

  /**
   * Calculates the shortest path between a source and destination factor
   * using Dijkstra's algorithm. The cost of a path is the minimized
   * precision of the conversion factors.
   */
  private List shortestPath(UnitType src, UnitType dst) {
    Heap heap = new Heap<>(new Edge[] { new Edge(src, 0) }, UnitFactors::compare);
    Map edges = new EnumMap<>(UnitType.class);
    edges.put(src, new Cost(0, null));

    while (!heap.empty()) {
      Edge e = heap.pop();
      UnitType edge = e.edge;
      int cost = e.cost;
      if (edge == dst) {
        break;
      }

      Map nbr = this.graph.get(edge);
      if (nbr == null) {
        continue;
      }

      for (UnitType n : nbr.keySet()) {
        Rational r = nbr.get(n);
        int newcost = cost + precision(r);
        Cost path = edges.get(n);
        if (path == null || newcost < path.cost) {
          edges.put(n, new Cost(newcost, edge));
          heap.push(new Edge(n, newcost));
        }
      }
    }

    Cost res = edges.get(dst);
    return res == null ? null : extractPath(edges, dst);
  }

  private static int compare(Edge a, Edge b) {
    return a.cost < b.cost ? -1 : a.cost > b.cost ? 1 : 0;
  }

  private static int precision(Rational r) {
    Decimal n = r.numerator();
    Decimal d = r.denominator();
    return (n.precision() + n.alignexp()) + (d.precision() + d.alignexp());
  }

  private static List extractPath(Map edges, UnitType dst) {
    List r = new ArrayList<>();
    for (UnitType c = dst; c != null; c = edges.get(c).previous) {
      r.add(c);
    }
    Collections.reverse(r);
    return r;
  }

  @AllArgsConstructor
  @ToString
  private static class Cost {
    public int cost;
    public UnitType previous;
  }

  @AllArgsConstructor
  @ToString
  private static class Edge {
    public UnitType edge;
    public int cost;
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy