1 | #include "sass.hpp" |
2 | #include <map> |
3 | #include <stdexcept> |
4 | #include <algorithm> |
5 | #include "units.hpp" |
6 | #include "error_handling.hpp" |
7 | |
8 | namespace Sass { |
9 | |
10 | /* the conversion matrix can be readed the following way */ |
11 | /* if you go down, the factor is for the numerator (multiply) */ |
12 | /* if you go right, the factor is for the denominator (divide) */ |
13 | /* and yes, we actually use both, not sure why, but why not!? */ |
14 | |
15 | const double size_conversion_factors[6][6] = |
16 | { |
17 | /* in cm pc mm pt px */ |
18 | /* in */ { 1, 2.54, 6, 25.4, 72, 96, }, |
19 | /* cm */ { 1.0/2.54, 1, 6.0/2.54, 10, 72.0/2.54, 96.0/2.54 }, |
20 | /* pc */ { 1.0/6.0, 2.54/6.0, 1, 25.4/6.0, 72.0/6.0, 96.0/6.0 }, |
21 | /* mm */ { 1.0/25.4, 1.0/10.0, 6.0/25.4, 1, 72.0/25.4, 96.0/25.4 }, |
22 | /* pt */ { 1.0/72.0, 2.54/72.0, 6.0/72.0, 25.4/72.0, 1, 96.0/72.0 }, |
23 | /* px */ { 1.0/96.0, 2.54/96.0, 6.0/96.0, 25.4/96.0, 72.0/96.0, 1, } |
24 | }; |
25 | |
26 | const double angle_conversion_factors[4][4] = |
27 | { |
28 | /* deg grad rad turn */ |
29 | /* deg */ { 1, 40.0/36.0, PI/180.0, 1.0/360.0 }, |
30 | /* grad */ { 36.0/40.0, 1, PI/200.0, 1.0/400.0 }, |
31 | /* rad */ { 180.0/PI, 200.0/PI, 1, 0.5/PI }, |
32 | /* turn */ { 360.0, 400.0, 2.0*PI, 1 } |
33 | }; |
34 | |
35 | const double time_conversion_factors[2][2] = |
36 | { |
37 | /* s ms */ |
38 | /* s */ { 1, 1000.0 }, |
39 | /* ms */ { 1/1000.0, 1 } |
40 | }; |
41 | const double frequency_conversion_factors[2][2] = |
42 | { |
43 | /* Hz kHz */ |
44 | /* Hz */ { 1, 1/1000.0 }, |
45 | /* kHz */ { 1000.0, 1 } |
46 | }; |
47 | const double resolution_conversion_factors[3][3] = |
48 | { |
49 | /* dpi dpcm dppx */ |
50 | /* dpi */ { 1, 1/2.54, 1/96.0 }, |
51 | /* dpcm */ { 2.54, 1, 2.54/96 }, |
52 | /* dppx */ { 96, 96/2.54, 1 } |
53 | }; |
54 | |
55 | UnitClass get_unit_type(UnitType unit) |
56 | { |
57 | switch (unit & 0xFF00) |
58 | { |
59 | case UnitClass::LENGTH: return UnitClass::LENGTH; |
60 | case UnitClass::ANGLE: return UnitClass::ANGLE; |
61 | case UnitClass::TIME: return UnitClass::TIME; |
62 | case UnitClass::FREQUENCY: return UnitClass::FREQUENCY; |
63 | case UnitClass::RESOLUTION: return UnitClass::RESOLUTION; |
64 | default: return UnitClass::INCOMMENSURABLE; |
65 | } |
66 | }; |
67 | |
68 | sass::string get_unit_class(UnitType unit) |
69 | { |
70 | switch (unit & 0xFF00) |
71 | { |
72 | case UnitClass::LENGTH: return "LENGTH" ; |
73 | case UnitClass::ANGLE: return "ANGLE" ; |
74 | case UnitClass::TIME: return "TIME" ; |
75 | case UnitClass::FREQUENCY: return "FREQUENCY" ; |
76 | case UnitClass::RESOLUTION: return "RESOLUTION" ; |
77 | default: return "INCOMMENSURABLE" ; |
78 | } |
79 | }; |
80 | |
81 | UnitType get_main_unit(const UnitClass unit) |
82 | { |
83 | switch (unit) |
84 | { |
85 | case UnitClass::LENGTH: return UnitType::PX; |
86 | case UnitClass::ANGLE: return UnitType::DEG; |
87 | case UnitClass::TIME: return UnitType::SEC; |
88 | case UnitClass::FREQUENCY: return UnitType::HERTZ; |
89 | case UnitClass::RESOLUTION: return UnitType::DPI; |
90 | default: return UnitType::UNKNOWN; |
91 | } |
92 | }; |
93 | |
94 | UnitType string_to_unit(const sass::string& s) |
95 | { |
96 | // size units |
97 | if (s == "px" ) return UnitType::PX; |
98 | else if (s == "pt" ) return UnitType::PT; |
99 | else if (s == "pc" ) return UnitType::PC; |
100 | else if (s == "mm" ) return UnitType::MM; |
101 | else if (s == "cm" ) return UnitType::CM; |
102 | else if (s == "in" ) return UnitType::IN; |
103 | // angle units |
104 | else if (s == "deg" ) return UnitType::DEG; |
105 | else if (s == "grad" ) return UnitType::GRAD; |
106 | else if (s == "rad" ) return UnitType::RAD; |
107 | else if (s == "turn" ) return UnitType::TURN; |
108 | // time units |
109 | else if (s == "s" ) return UnitType::SEC; |
110 | else if (s == "ms" ) return UnitType::MSEC; |
111 | // frequency units |
112 | else if (s == "Hz" ) return UnitType::HERTZ; |
113 | else if (s == "kHz" ) return UnitType::KHERTZ; |
114 | // resolutions units |
115 | else if (s == "dpi" ) return UnitType::DPI; |
116 | else if (s == "dpcm" ) return UnitType::DPCM; |
117 | else if (s == "dppx" ) return UnitType::DPPX; |
118 | // for unknown units |
119 | else return UnitType::UNKNOWN; |
120 | } |
121 | |
122 | const char* unit_to_string(UnitType unit) |
123 | { |
124 | switch (unit) { |
125 | // size units |
126 | case UnitType::PX: return "px" ; |
127 | case UnitType::PT: return "pt" ; |
128 | case UnitType::PC: return "pc" ; |
129 | case UnitType::MM: return "mm" ; |
130 | case UnitType::CM: return "cm" ; |
131 | case UnitType::IN: return "in" ; |
132 | // angle units |
133 | case UnitType::DEG: return "deg" ; |
134 | case UnitType::GRAD: return "grad" ; |
135 | case UnitType::RAD: return "rad" ; |
136 | case UnitType::TURN: return "turn" ; |
137 | // time units |
138 | case UnitType::SEC: return "s" ; |
139 | case UnitType::MSEC: return "ms" ; |
140 | // frequency units |
141 | case UnitType::HERTZ: return "Hz" ; |
142 | case UnitType::KHERTZ: return "kHz" ; |
143 | // resolutions units |
144 | case UnitType::DPI: return "dpi" ; |
145 | case UnitType::DPCM: return "dpcm" ; |
146 | case UnitType::DPPX: return "dppx" ; |
147 | // for unknown units |
148 | default: return "" ; |
149 | } |
150 | } |
151 | |
152 | sass::string unit_to_class(const sass::string& s) |
153 | { |
154 | if (s == "px" ) return "LENGTH" ; |
155 | else if (s == "pt" ) return "LENGTH" ; |
156 | else if (s == "pc" ) return "LENGTH" ; |
157 | else if (s == "mm" ) return "LENGTH" ; |
158 | else if (s == "cm" ) return "LENGTH" ; |
159 | else if (s == "in" ) return "LENGTH" ; |
160 | // angle units |
161 | else if (s == "deg" ) return "ANGLE" ; |
162 | else if (s == "grad" ) return "ANGLE" ; |
163 | else if (s == "rad" ) return "ANGLE" ; |
164 | else if (s == "turn" ) return "ANGLE" ; |
165 | // time units |
166 | else if (s == "s" ) return "TIME" ; |
167 | else if (s == "ms" ) return "TIME" ; |
168 | // frequency units |
169 | else if (s == "Hz" ) return "FREQUENCY" ; |
170 | else if (s == "kHz" ) return "FREQUENCY" ; |
171 | // resolutions units |
172 | else if (s == "dpi" ) return "RESOLUTION" ; |
173 | else if (s == "dpcm" ) return "RESOLUTION" ; |
174 | else if (s == "dppx" ) return "RESOLUTION" ; |
175 | // for unknown units |
176 | return "CUSTOM:" + s; |
177 | } |
178 | |
179 | // throws incompatibleUnits exceptions |
180 | double conversion_factor(const sass::string& s1, const sass::string& s2) |
181 | { |
182 | // assert for same units |
183 | if (s1 == s2) return 1; |
184 | // get unit enum from string |
185 | UnitType u1 = string_to_unit(s: s1); |
186 | UnitType u2 = string_to_unit(s: s2); |
187 | // query unit group types |
188 | UnitClass t1 = get_unit_type(unit: u1); |
189 | UnitClass t2 = get_unit_type(unit: u2); |
190 | // return the conversion factor |
191 | return conversion_factor(u1, u2, t1, t2); |
192 | } |
193 | |
194 | // throws incompatibleUnits exceptions |
195 | double conversion_factor(UnitType u1, UnitType u2, UnitClass t1, UnitClass t2) |
196 | { |
197 | // can't convert between groups |
198 | if (t1 != t2) return 0; |
199 | // get absolute offset |
200 | // used for array acces |
201 | size_t i1 = u1 - t1; |
202 | size_t i2 = u2 - t2; |
203 | // process known units |
204 | switch (t1) { |
205 | case LENGTH: |
206 | return size_conversion_factors[i1][i2]; |
207 | case ANGLE: |
208 | return angle_conversion_factors[i1][i2]; |
209 | case TIME: |
210 | return time_conversion_factors[i1][i2]; |
211 | case FREQUENCY: |
212 | return frequency_conversion_factors[i1][i2]; |
213 | case RESOLUTION: |
214 | return resolution_conversion_factors[i1][i2]; |
215 | case INCOMMENSURABLE: |
216 | return 0; |
217 | } |
218 | // fallback |
219 | return 0; |
220 | } |
221 | |
222 | double convert_units(const sass::string& lhs, const sass::string& rhs, int& lhsexp, int& rhsexp) |
223 | { |
224 | double f = 0; |
225 | // do not convert same ones |
226 | if (lhs == rhs) return 0; |
227 | // skip already canceled out unit |
228 | if (lhsexp == 0) return 0; |
229 | if (rhsexp == 0) return 0; |
230 | // check if it can be converted |
231 | UnitType ulhs = string_to_unit(s: lhs); |
232 | UnitType urhs = string_to_unit(s: rhs); |
233 | // skip units we cannot convert |
234 | if (ulhs == UNKNOWN) return 0; |
235 | if (urhs == UNKNOWN) return 0; |
236 | // query unit group types |
237 | UnitClass clhs = get_unit_type(unit: ulhs); |
238 | UnitClass crhs = get_unit_type(unit: urhs); |
239 | // skip units we cannot convert |
240 | if (clhs != crhs) return 0; |
241 | // if right denominator is bigger than lhs, we want to keep it in rhs unit |
242 | if (rhsexp < 0 && lhsexp > 0 && - rhsexp > lhsexp) { |
243 | // get the conversion factor for units |
244 | f = conversion_factor(u1: urhs, u2: ulhs, t1: clhs, t2: crhs); |
245 | // left hand side has been consumned |
246 | f = std::pow(x: f, y: lhsexp); |
247 | rhsexp += lhsexp; |
248 | lhsexp = 0; |
249 | } |
250 | else { |
251 | // get the conversion factor for units |
252 | f = conversion_factor(u1: ulhs, u2: urhs, t1: clhs, t2: crhs); |
253 | // right hand side has been consumned |
254 | f = std::pow(x: f, y: rhsexp); |
255 | lhsexp += rhsexp; |
256 | rhsexp = 0; |
257 | } |
258 | return f; |
259 | } |
260 | |
261 | bool Units::operator< (const Units& rhs) const |
262 | { |
263 | return (numerators < rhs.numerators) && |
264 | (denominators < rhs.denominators); |
265 | } |
266 | bool Units::operator== (const Units& rhs) const |
267 | { |
268 | return (numerators == rhs.numerators) && |
269 | (denominators == rhs.denominators); |
270 | } |
271 | bool Units::operator!= (const Units& rhs) const |
272 | { |
273 | return ! (*this == rhs); |
274 | } |
275 | |
276 | double Units::normalize() |
277 | { |
278 | |
279 | size_t iL = numerators.size(); |
280 | size_t nL = denominators.size(); |
281 | |
282 | // the final conversion factor |
283 | double factor = 1; |
284 | |
285 | for (size_t i = 0; i < iL; i++) { |
286 | sass::string &lhs = numerators[i]; |
287 | UnitType ulhs = string_to_unit(s: lhs); |
288 | if (ulhs == UNKNOWN) continue; |
289 | UnitClass clhs = get_unit_type(unit: ulhs); |
290 | UnitType umain = get_main_unit(unit: clhs); |
291 | if (ulhs == umain) continue; |
292 | double f(conversion_factor(u1: umain, u2: ulhs, t1: clhs, t2: clhs)); |
293 | if (f == 0) throw std::runtime_error("INVALID" ); |
294 | numerators[i] = unit_to_string(unit: umain); |
295 | factor /= f; |
296 | } |
297 | |
298 | for (size_t n = 0; n < nL; n++) { |
299 | sass::string &rhs = denominators[n]; |
300 | UnitType urhs = string_to_unit(s: rhs); |
301 | if (urhs == UNKNOWN) continue; |
302 | UnitClass crhs = get_unit_type(unit: urhs); |
303 | UnitType umain = get_main_unit(unit: crhs); |
304 | if (urhs == umain) continue; |
305 | double f(conversion_factor(u1: umain, u2: urhs, t1: crhs, t2: crhs)); |
306 | if (f == 0) throw std::runtime_error("INVALID" ); |
307 | denominators[n] = unit_to_string(unit: umain); |
308 | factor /= f; |
309 | } |
310 | |
311 | std::sort (first: numerators.begin(), last: numerators.end()); |
312 | std::sort (first: denominators.begin(), last: denominators.end()); |
313 | |
314 | // return for conversion |
315 | return factor; |
316 | } |
317 | |
318 | double Units::reduce() |
319 | { |
320 | |
321 | size_t iL = numerators.size(); |
322 | size_t nL = denominators.size(); |
323 | |
324 | // have less than two units? |
325 | if (iL + nL < 2) return 1; |
326 | |
327 | // first make sure same units cancel each other out |
328 | // it seems that a map table will fit nicely to do this |
329 | // we basically construct exponents for each unit |
330 | // has the advantage that they will be pre-sorted |
331 | std::map<sass::string, int> exponents; |
332 | |
333 | // initialize by summing up occurrences in unit vectors |
334 | // this will already cancel out equivalent units (e.q. px/px) |
335 | for (size_t i = 0; i < iL; i ++) exponents[numerators[i]] += 1; |
336 | for (size_t n = 0; n < nL; n ++) exponents[denominators[n]] -= 1; |
337 | |
338 | // the final conversion factor |
339 | double factor = 1; |
340 | |
341 | // convert between compatible units |
342 | for (size_t i = 0; i < iL; i++) { |
343 | for (size_t n = 0; n < nL; n++) { |
344 | sass::string &lhs = numerators[i], &rhs = denominators[n]; |
345 | int &lhsexp = exponents[lhs], &rhsexp = exponents[rhs]; |
346 | double f(convert_units(lhs, rhs, lhsexp, rhsexp)); |
347 | if (f == 0) continue; |
348 | factor /= f; |
349 | } |
350 | } |
351 | |
352 | // now we can build up the new unit arrays |
353 | numerators.clear(); |
354 | denominators.clear(); |
355 | |
356 | // recreate sorted units vectors |
357 | for (auto exp : exponents) { |
358 | int &exponent = exp.second; |
359 | while (exponent > 0 && exponent --) |
360 | numerators.push_back(x: exp.first); |
361 | while (exponent < 0 && exponent ++) |
362 | denominators.push_back(x: exp.first); |
363 | } |
364 | |
365 | // return for conversion |
366 | return factor; |
367 | |
368 | } |
369 | |
370 | sass::string Units::unit() const |
371 | { |
372 | sass::string u; |
373 | size_t iL = numerators.size(); |
374 | size_t nL = denominators.size(); |
375 | for (size_t i = 0; i < iL; i += 1) { |
376 | if (i) u += '*'; |
377 | u += numerators[i]; |
378 | } |
379 | if (nL != 0) u += '/'; |
380 | for (size_t n = 0; n < nL; n += 1) { |
381 | if (n) u += '*'; |
382 | u += denominators[n]; |
383 | } |
384 | return u; |
385 | } |
386 | |
387 | bool Units::is_unitless() const |
388 | { |
389 | return numerators.empty() && |
390 | denominators.empty(); |
391 | } |
392 | |
393 | bool Units::is_valid_css_unit() const |
394 | { |
395 | return numerators.size() <= 1 && |
396 | denominators.size() == 0; |
397 | } |
398 | |
399 | // this does not cover all cases (multiple preferred units) |
400 | double Units::convert_factor(const Units& r) const |
401 | { |
402 | |
403 | sass::vector<sass::string> miss_nums(0); |
404 | sass::vector<sass::string> miss_dens(0); |
405 | // create copy since we need these for state keeping |
406 | sass::vector<sass::string> r_nums(r.numerators); |
407 | sass::vector<sass::string> r_dens(r.denominators); |
408 | |
409 | auto l_num_it = numerators.begin(); |
410 | auto l_num_end = numerators.end(); |
411 | |
412 | bool l_unitless = is_unitless(); |
413 | auto r_unitless = r.is_unitless(); |
414 | |
415 | // overall conversion |
416 | double factor = 1; |
417 | |
418 | // process all left numerators |
419 | while (l_num_it != l_num_end) |
420 | { |
421 | // get and increment afterwards |
422 | const sass::string l_num = *(l_num_it ++); |
423 | |
424 | auto r_num_it = r_nums.begin(), r_num_end = r_nums.end(); |
425 | |
426 | bool found = false; |
427 | // search for compatible numerator |
428 | while (r_num_it != r_num_end) |
429 | { |
430 | // get and increment afterwards |
431 | const sass::string r_num = *(r_num_it); |
432 | // get possible conversion factor for units |
433 | double conversion = conversion_factor(s1: l_num, s2: r_num); |
434 | // skip incompatible numerator |
435 | if (conversion == 0) { |
436 | ++ r_num_it; |
437 | continue; |
438 | } |
439 | // apply to global factor |
440 | factor *= conversion; |
441 | // remove item from vector |
442 | r_nums.erase(position: r_num_it); |
443 | // found numerator |
444 | found = true; |
445 | break; |
446 | } |
447 | // maybe we did not find any |
448 | // left numerator is leftover |
449 | if (!found) miss_nums.push_back(x: l_num); |
450 | } |
451 | |
452 | auto l_den_it = denominators.begin(); |
453 | auto l_den_end = denominators.end(); |
454 | |
455 | // process all left denominators |
456 | while (l_den_it != l_den_end) |
457 | { |
458 | // get and increment afterwards |
459 | const sass::string l_den = *(l_den_it ++); |
460 | |
461 | auto r_den_it = r_dens.begin(); |
462 | auto r_den_end = r_dens.end(); |
463 | |
464 | bool found = false; |
465 | // search for compatible denominator |
466 | while (r_den_it != r_den_end) |
467 | { |
468 | // get and increment afterwards |
469 | const sass::string r_den = *(r_den_it); |
470 | // get possible conversion factor for units |
471 | double conversion = conversion_factor(s1: l_den, s2: r_den); |
472 | // skip incompatible denominator |
473 | if (conversion == 0) { |
474 | ++ r_den_it; |
475 | continue; |
476 | } |
477 | // apply to global factor |
478 | factor /= conversion; |
479 | // remove item from vector |
480 | r_dens.erase(position: r_den_it); |
481 | // found denominator |
482 | found = true; |
483 | break; |
484 | } |
485 | // maybe we did not find any |
486 | // left denominator is leftover |
487 | if (!found) miss_dens.push_back(x: l_den); |
488 | } |
489 | |
490 | // check left-overs (ToDo: might cancel out?) |
491 | if (miss_nums.size() > 0 && !r_unitless) { |
492 | throw Exception::IncompatibleUnits(r, *this); |
493 | } |
494 | else if (miss_dens.size() > 0 && !r_unitless) { |
495 | throw Exception::IncompatibleUnits(r, *this); |
496 | } |
497 | else if (r_nums.size() > 0 && !l_unitless) { |
498 | throw Exception::IncompatibleUnits(r, *this); |
499 | } |
500 | else if (r_dens.size() > 0 && !l_unitless) { |
501 | throw Exception::IncompatibleUnits(r, *this); |
502 | } |
503 | |
504 | return factor; |
505 | } |
506 | |
507 | } |
508 | |