-
Notifications
You must be signed in to change notification settings - Fork 0
/
conversion.js
231 lines (193 loc) · 7.62 KB
/
conversion.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
/*
// Conversion Class - for Technical Assessment
//
// This class provides a Fluent Interface (aka chaining) to convert between like units
// of measurement. It will generate a TypeError if the wrong arguments are presented, and a
// TypeError if conflicting arguments (e.g. kg to feet) are presented.
//
// It also throws a Syntax error if you give it something like "Frozbozz" as a type.
//
// Valid conversions are:
//
// Pounds (lb), Kilograms (kg), Ounces (oz)
// Feet (ft), Inches (in), Meters (m)
// Pint (pt), Fluid Ounces (fl oz), Bombers (NO ABBREVIATION ALLOWED BECAUSE THEY ARE BIG)
//
// Example usage:
// var kilograms = new Conversion()
// .convert(16, "lb")
// .to("kg")
// .execute()
//
// Assumptions:
// * Your conversions will fit within a numeric representation in Node/JS - 51 bits of precision in floats/mantissa!
// * You are sending down a string as per the original
// Note that the Fluent/chaining functions (convert, to) will return an instance of Conversion, and execute will
// return a n.2 float, rounded to the neared 100th place.
*/
//JS doesn't have true enumerated types, but they make it easy to convert variable strings into a well understood type
//and then reference that by name.
Conversion.UNIT = {
POUND: 1,
KILOGRAM: 2,
OUNCE: 3,
FOOT: 4,
INCH: 5,
METER: 6,
PINT: 16, //because we do drink 16 fl oz of beer after all.
FLUID_OUNCE: 17,
BOMBER: 20
}
Conversion.TYPE = {
WEIGHT: 1,
LENGTH: 2,
VOLUME: 3
}
// this allows a lookup based on source.destination in a hash to get the conversion constant.
// normally I could just convert from A to B. But a smaller data structure (with less room for
// error) allows me to convert from a A to Z to B. The advantage here is going to a normalized
// unit, and then all I have to do is convert from normalized to my final unit.
//
// This has the expense of one additional multiply vs. smaller storage, and storage that would
// scale with a huge number of conversions. Basically if I had 30 different types of weight, then
// I would need a 30x30 matrix to handle the conversions, and that's starting to be inefficient. I can
// store it in a hash which gives me o(1) lookup, but still...
//
// another interesting issue, which I didn't encounter, but could be an issue with floats, is that if
// I convert using imprecisely represented conversions, then I might somehow say that 24 inches = 1.999999999999 feet.
// there's a trick I employ below to try to avoid this.
Conversion.normalizers = {};
//convert all weight to kilos
Conversion.normalizers[Conversion.UNIT.POUND] = 1.0/2.2;
Conversion.normalizers[Conversion.UNIT.KILOGRAM] = 1;
Conversion.normalizers[Conversion.UNIT.OUNCE] = 1/(2.2 * 16);
//convert all length to meters
Conversion.normalizers[Conversion.UNIT.FOOT] = 0.3;
Conversion.normalizers[Conversion.UNIT.INCH] = 0.3/12;
Conversion.normalizers[Conversion.UNIT.METER] = 1;
//store everything as fluid ounces
Conversion.normalizers[Conversion.UNIT.PINT] = 16;
Conversion.normalizers[Conversion.UNIT.FLUID_OUNCE] = 1;
Conversion.normalizers[Conversion.UNIT.BOMBER] = 20;
//we may also want to add a snifter at smoe point, but I can't recall if they are 8 or 10 oz.
function Conversion () {
this.sourceUnit = null; //a hash/json of type, unit enums
this.targetUnit = null; //a hash/json of type/unit enums
this.sourceValue = null;
return this;
}
//to make this extensible, we could create either a regexp engine or create a set of hashes that convert, and then
//just lookup the result directly. Might be a simpler routine. But frankly, KISS for right now and don't use indirection when
//the problem isn't large. Deal with that sort of refactor when you need it.
Conversion.prototype.validateUnit = function(unitStr) {
var self = this;
//at some later date it might be nice to allow units as the enumerated types above.
unit = unitStr.toLowerCase(); //see how we converted that camelcase? ;)
switch (unit) {
//weight types
case "lb": case "lbs": case "pounds": case "pound": //I hate how my editor doesn't indent case here...
return {
"type": Conversion.TYPE.WEIGHT,
"unit": Conversion.UNIT.POUND
}
break;
case "kg": case "kilogram": case "kilo": case "kilos": case "kilograms":
return {
"type": Conversion.TYPE.WEIGHT,
"unit": Conversion.UNIT.KILOGRAM
};
break;
case "oz": case "ounce": case "ounces":
return {
"type": Conversion.TYPE.WEIGHT,
"unit": Conversion.UNIT.KILOGRAM
};
break;
//Length types
case "ft": case "foot": case "feet":
return {
"type": Conversion.TYPE.LENGTH,
"unit": Conversion.UNIT.FOOT
};
break;
case "in": case "inch": case "inches":
return {
"type": Conversion.TYPE.LENGTH,
"unit": Conversion.UNIT.INCH
};
break;
case "m": case "meter": case "meters":
return {
"type": Conversion.TYPE.LENGTH,
"unit": Conversion.UNIT.METER
};
break;
//volume types
case "pt": case "pint": case "pints":
return {
"type": Conversion.TYPE.VOLUME,
"unit": Conversion.UNIT.PINT
}
break;
case "floz": case "fl oz": case "fluid ounce": case "fluid ounces": case "fluidounce": case "fluid ounce":
return {
"type": Conversion.TYPE.VOLUME,
"unit": Conversion.UNIT.FLUID_OUNCE
};
break;
case "bomber": case "bombers":
return {
"type": Conversion.TYPE.VOLUME,
"unit": Conversion.UNIT.BOMBER
};
break;
default:
throw new SyntaxError("Unit: " + unitStr + " not valid");
}
}
Conversion.prototype.convert = function(amount, unitStr) {
//add argument checks here. I can add a type of for amount and unitstr to ensure float/str respectively.
var self = this;
self.sourceUnit = self.validateUnit(unitStr);
self.sourceValue = amount;
return self;
}
Conversion.prototype.to = function(unitStr) {
var self = this;
self.targetUnit = self.validateUnit(unitStr);
return self;
}
Conversion.prototype.execute = function() {
var self = this;
//perform a few checks.
if (self.sourceUnit == null) {
throw new TypeError("Failed to supply a source unit");
return null;
} else if (self.targetUnit == null) {
throw new TypeError("Failed to supply a target unit");
return null;
} else if (self.sourceValue == null) {
//this should never happen, but let's cover it in case someone manually sets a variable.
throw new TypeError("No source value set");
return null;
} else if (self.targetUnit.type != self.sourceUnit.type) {
throw new TypeError("Target type != Source type");
return null;
} else {
//do the actual conversion.
/* note - original implementation did this - but it could cause imprecision in floats. Instead, I'll create a conversion ratio.
//convert the item into the our "metric" equivalent, and then invert that to the target of interest.
var intermediate = Conversion.normalizers[self.sourceUnit.unit] * self.sourceValue;
return intermediate / Conversion.normalizers[self.targetUnit.unit];
*/
var conversion_ratio = Conversion.normalizers[self.sourceUnit.unit] / Conversion.normalizers[self.targetUnit.unit];
var result = self.sourceValue * conversion_ratio;
// I'd prefer to send the full float down, so somebody else consuming it can retain whatever precision I still have
// and make a final decision. But it's in the
// requirements, so we're doing it. I'd actually go back to product/redefine the requirements in real life, but F'ake la vie.
// http://stackoverflow.com/questions/11832914/round-to-at-most-2-decimal-places
var rounded = +result.toFixed(2);
return rounded;
}
}
module.exports= Conversion;