Coverage Report

Created: 2026-04-15 06:05

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