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 }