1 module dunitconversion.convertor;
2 
3 import dunitconversion.linearfunction;
4 import dunitconversion.conversionfamily;
5 import dunitconversion.conversionrule;
6 import dunitconversion.aliasdictionary;
7 
8 import std.json;
9 import std.file;
10 import std.math;
11 import std.conv;
12 
13 /**
14  * The UnitConvertor class provides tool for converting units stored
15  * in a string form. It uses "base" unit for each "family" (length, speed etc)
16  * and perform conversions inside a family through conversion to and from base unit.
17  */
18 class UnitConvertor {
19 
20     /// Default constructor, creates an empty convertor
21     this () {
22         m_aliases = new AliasDictionary;
23     }
24 
25     /**
26      * Checks if unit conversion from in unit to out unit is possible
27      * Params:
28      *      inUnit = unit to convert from
29      *      outUnit = unit to convert to
30      * Returns:
31      *      true if conversion is possible, false otherwise
32      */
33     bool canConvert(string inUnit, string outUnit) const {
34         return convert(inUnit, outUnit).isValid();
35     }
36 
37     /**
38      * Converts from in unit to out unit
39      * Params:
40      *      inUnit = unit to convert from
41      *      outUnit = unit to convert to
42      * Returns:
43      *      LinearFunction object containing conversion from in to out unit
44      */
45     LinearFunction convert(string inUnit, string outUnit) const {
46         if (inUnit == outUnit)
47             return LinearFunction(1, 0);
48         string actualIn, actualOut;
49         if (inUnit in m_aliases)
50             actualIn = m_aliases.name(inUnit);
51         else
52             actualIn = inUnit;
53         if (outUnit in m_aliases)
54             actualOut = m_aliases.name(outUnit);
55         else
56             actualOut = outUnit;
57 
58         string inFamily, outFamily;
59         try {
60             inFamily = m_familiesByUnit[actualIn];
61             outFamily = m_familiesByUnit[actualOut];
62         }
63         catch (Exception e) {
64             throw new Exception("Conversion from " ~ inUnit ~ " to " ~ outUnit ~ " not found");
65         }
66             
67         return m_families[inFamily].convert(actualIn, actualOut);
68     }
69 
70     /**
71      * Converts a given value from in unit to out unit
72      * Params:
73      *      value = value to convert
74      *      inUnit = unit to convert from
75      *      outUnit = unit to convert to
76      *      defaultValue = value to return if conversion fails
77      * Returns:
78      *      value converted to out unit
79      * Details: Supports aliases for unit names, see AliasDictionary
80      */
81     double convert(double value, string inUnit, string outUnit, double defaultValue = double.nan) const {
82         try {
83             LinearFunction f = convert(inUnit, outUnit);
84             return f.y(value);
85         }
86         catch (Exception e) {
87             return defaultValue;
88         }
89     }
90 
91     /**
92      * Deserializes unit conversion rules from JSON
93      * Params:
94      *      json = JSON object for deserialization
95      *
96      * Details: Note that this function does not clear the existing
97      * conversion allowing you to override or augment conversion rules
98      * from a number of different files, let's say, built-in conversions
99      * and user conversions`
100      */
101     void loadFromJson(JSONValue json) {
102         auto rules = json["rules"].array;
103         foreach (r; rules) {
104             auto rule = r.object;
105             auto baseUnit = rule["base"].str;
106             auto familyName = rule["family"].str;
107             auto conversions = rule["conversions"].array;
108             foreach (c; conversions) {
109                 auto conversion = c.object;
110                 auto unit = conversion["unit"].str;
111                 immutable double k = conversion.get("k", JSONValue(1)).toString().to!double;
112                 immutable double b = conversion.get("b", JSONValue(0)).toString().to!double;
113                 if (unit is null || approxEqual(k, 0))
114                     continue;
115                 addConversionRule(ConversionRule(familyName,
116                                                     baseUnit,
117                                                     unit,LinearFunction(k, b)
118                                                     ));
119             }
120         }
121     }
122 
123     /**
124      * Serializes current unit conversion rules to JSON
125      * Returns: JsonObject containing serialized rules
126      * Details: Not implemented yet
127      */
128     JSONValue toJson() const {
129         assert(false, "toJson method not implemented");
130     }
131 
132     /**
133      * Adds a conversion rule to convertor
134      * Params:
135      *      rule = rule to add
136      * Details: This function doesn't convert an alas for a unit to an actual unit name, so make sure to
137      * pass here an actual unit name
138      * Throws: Exception if a passed rule has existing family with different base unit,
139      * existing unit with different family or existing unit with a different family or base unit
140      */
141     void addConversionRule(ConversionRule rule) {
142         if (rule.family in m_baseUnitsByFamilies && m_baseUnitsByFamilies[rule.family] != rule.baseUnit)
143             throw new Exception("Incorrect rule added: incorrect family base unit");
144         if (rule.baseUnit in m_familiesByUnit && m_familiesByUnit[rule.baseUnit] != rule.family)
145             throw new Exception("Incorrect rule added: incorrect base unit family");
146         if (rule.unit in m_familiesByUnit && m_familiesByUnit[rule.unit] != rule.family)
147             throw new Exception("Incorrect rule added: incorrect unit family");
148         if (rule.family !in m_families)
149         {
150             auto family = new ConversionFamily;
151             family.addConversionRule(rule);
152             m_families[rule.family] = family;
153             m_baseUnitsByFamilies[rule.family] = rule.baseUnit;
154             m_familiesByUnit[rule.baseUnit] = rule.family;
155         }
156         else
157         {
158             m_families[rule.family].addConversionRule(rule);
159         }
160         m_familiesByUnit[rule.unit] = rule.family;
161     }
162 
163     /**
164      * Clears unit convertor removing all unit conversion rules
165      */
166     void clear() {
167         m_families.clear();
168         m_familiesByUnit.clear();
169         m_baseUnitsByFamilies.clear();
170         m_aliases.clear();
171     }
172 
173     /**
174      * Method provides access to a list of families of units in this convertor
175      * Returns: An array of strings containing all unit families
176      */
177     string[] families() const {
178         return m_families.keys();   
179     }
180 
181     /**
182      * Gets a family for a given unit
183      * Params: unit = unit to return a family
184      * Returns a family name
185      */
186     string family(string unit) const {
187         string actualUnit;
188         if (unit in m_aliases)
189             actualUnit = m_aliases.name(unit);
190         else
191             actualUnit = unit;
192         if (actualUnit in m_familiesByUnit) 
193             return m_familiesByUnit[actualUnit];
194         throw new Exception("Family name is not known for unit " ~ unit);
195     }
196 
197     /**
198      * Gets a list of units with a possible connection to/from a given unit
199      * Params: unit = unit to get a list of conversions
200      * Returns String list with units with possible conversion to a given unit, including a given unit. If
201      * conversion to/from a given unit is unknown returns an empty list
202      */
203     string[] conversions(string unit) const {
204         try {
205             return units(family(unit));
206         }
207         catch (Exception e) {
208             return null;
209         }
210     }
211 
212     /**
213      * Method provides access to a list of units in this convertor within a given
214      * family, effectively providing a list of unit with a possible conversion from
215      * any unit of this list to any other
216      * Params:
217      *   family = family to return unit list
218      * Returns: string[] containing a list of units known by this unit convertor
219      */
220     string[] units(string family) const {
221         string [] result;
222         foreach(item; m_familiesByUnit.byKeyValue()) {
223             if (item.value == family) {
224                 result ~= item.key;
225             }
226         }
227         return result;
228     }
229 
230     /**
231      * Loads unit aliases from json serialized object
232      * Params:
233      *      object = json-serialized aliases
234      */
235     void loadAliasesFromJson(JSONValue object) {
236         m_aliases.loadFromJson(object);
237     }
238 
239     /**
240      * Removes all alias rules
241      */
242     void clearAliases() {
243         m_aliases.clear();
244     }
245 
246     /**
247      * Gets unit name by alias using internal alias dictionary
248      * Params:
249      *      alias = unit alias to get unit name
250      * Returns: unit name or an empty string if a specified alias is not found
251      */
252     string unitName(string aliasName) const {
253         return m_aliases.name(aliasName);
254     }
255 
256 package:
257     string[string] m_familiesByUnit;   /// Key is a unit, Value is a corresponding family. Base units are also put here
258     string[string] m_baseUnitsByFamilies;  /// Key is a family name, Value is a corresponding base unit
259     ConversionFamily[string] m_families;   /// Key is a family name, Value is a family
260     AliasDictionary m_aliases;      /// Alias dictionary
261 }