File: ./info.txt
   1 chu [values/units...]
   2 
   3 CHange Units is a small app to convert between funny units and international
   4 units. All the commonly-used measurement units are supported.

     File: ./main.c
   1 #include <ctype.h>
   2 #include <fcntl.h>
   3 #include <math.h>
   4 #include <stdbool.h>
   5 #include <stdio.h>
   6 #include <stdlib.h>
   7 #include <string.h>
   8 
   9 // all the unit categories, which are used to style the output
  10 enum {
  11     short_length_unit = 0,
  12     longer_length_unit = 1,
  13     area_unit = 2,
  14     other_unit = 3,
  15     volume_unit = 4,
  16     weight_unit = 5,
  17     pressure_unit = 6,
  18     temperature_unit = 7,
  19 };
  20 
  21 // styles is a list of all ANSI-styles for the various unit categories
  22 char* styles[] = {
  23     // short-length units
  24     "\x1b[38;5;35m",
  25     // longer-length units
  26     "\x1b[38;5;29m",
  27     // area
  28     "\x1b[38;5;58m",
  29     // other units
  30     "\x1b[38;5;39m",
  31     // volume
  32     "\x1b[38;5;31m",
  33     // weight
  34     "\x1b[38;5;208m",
  35     // pressure
  36     "\x1b[38;5;99m",
  37     // temperature
  38     "\x1b[38;5;160m",
  39 };
  40 
  41 // alias is an alternative name for the name of a unit
  42 typedef struct alias {
  43     char* src;
  44     char* dst;
  45 } alias;
  46 
  47 // aliases are all replacements supported for units involving superscripts
  48 alias aliases[] = {
  49     {"m2", ""},
  50     {"in2", "in²"},
  51     {"ft2", "ft²"},
  52     {"km2","km²"},
  53     {"mi2", "mi²"},
  54     {"m3", ""},
  55     {"ft3", "ft³"},
  56     {"kg/m2","kg/m²"},
  57 };
  58 
  59 // pair describes both ways to convert between 2 units
  60 typedef struct pair {
  61     // src is the name of the `from` unit
  62     char* src;
  63 
  64     // dst is the name of the `to` unit
  65     char* dst;
  66 
  67     // mul is the direct conversion's multiplication factor
  68     double mul;
  69 
  70     // add is the direct conversion's bias amount, and is only
  71     // needed because of temperature units like fahrenheit and kelvin
  72     double add;
  73 
  74     // kind is these units' category, and is used to style output
  75     size_t kind;
  76 } pair;
  77 
  78 // conversions has data for all unit-conversions this app supports
  79 const pair conversions[] = {
  80     {"cm", "in", 2.54, 0, short_length_unit},
  81     {"m", "yd", 1.093613, 0, short_length_unit},
  82     {"m", "ft", 3.28084, 0, short_length_unit},
  83     {"km", "mi", 0.6213712, 0, longer_length_unit},
  84     {"km", "nmi", 1 / 1.852, 0, longer_length_unit},
  85     {"km/h", "mi/h", 0.6213712, 0, longer_length_unit},
  86     {"", "ft²", 3.28084 * 3.28084, 0, area_unit},
  87     {"", "in²", 1 / 1550.00310001, 0, area_unit},
  88     {"", "ac", 1 / 4046.873, 0, area_unit},
  89     {"km²", "mi²", 1 / 2.5899881103360, 0, area_unit},
  90     {"km²", "ac", 1 / 247.10538161, 0, area_unit},
  91     {"kpl", "mpg", 0.6213712 / 0.2199692, 0, other_unit},
  92     {"mL", "oz", 1 / 29.5735295625, 0, other_unit},
  93     {"L", "cup", 1 / 0.2365882365, 0, volume_unit},
  94     {"L", "gal", 0.2199692, 0, volume_unit},
  95     {"", "ft³", 3.28084 * 3.28084 * 3.28084, 0, volume_unit},
  96     {"kg", "lb", 2.204623, 0, weight_unit},
  97     {"kg", "ton", 1 / 907.18474, 0, weight_unit},
  98     {"Pa", "psi", 0.00014503773800722, 0, pressure_unit},
  99     {"kg/m²", "psi", 1 / 703.069579639159, 0, pressure_unit},
 100     {"C", "F", 9.0 / 5.0, 32.0, temperature_unit},
 101     {"K", "C", 1.0, -273.15, temperature_unit},
 102 };
 103 
 104 // strcmpins is the ASCII case-insensitive counterpart to the stdlib's strcmp
 105 int strcmpins(char* x, char* y) {
 106     for (size_t i = 0; true; i++) {
 107         int diff = tolower((int)x[i]) - tolower((int)y[i]);
 108         if (diff != 0) {
 109             return diff;
 110         }
 111 
 112         if (x[i] == 0) {
 113             return 0;
 114         }
 115     }
 116 }
 117 
 118 // has_prefix checks if a string starts with the prefix given
 119 bool has_prefix(char* s, char* prefix) {
 120     for (size_t i = 0; true; i++) {
 121         if (s[i] == 0) {
 122             return prefix[i] == 0;
 123         }
 124         if (prefix[i] == 0) {
 125             return true;
 126         }
 127         if (s[i] != prefix[i]) {
 128             return false;
 129         }
 130     }
 131 }
 132 
 133 // try_number tries to parse a float64 number from the string given
 134 bool try_number(char* s, double* n) {
 135     if (strcmp(s, "0") == 0 || strcmp(s, "0.0") == 0) {
 136         *= 0;
 137         return true;
 138     }
 139 
 140     for (size_t i = 0; s[i] != 0; i++) {
 141         if ('0' <= s[i] && s[i] <= '9') {
 142             continue;
 143         }
 144         if (s[i] == '.') {
 145             continue;
 146         }
 147         return false;
 148     }
 149 
 150 
 151     double v = atof(s);
 152     if (!isnan(v) && !isinf(v) && v != 0) {
 153         *= v;
 154         return true;
 155     }
 156     return false;
 157 }
 158 
 159 // write_bin emits the binary representation of a non-negative number as ASCII
 160 // values, by changing the buffer given
 161 char* write_bin(char* s, size_t max, unsigned long long n) {
 162     if (< 1) {
 163         s[0] = '0';
 164         s[1] = 0;
 165         return s;
 166     }
 167 
 168     memset(s, 0, max);
 169     s += max - 1;
 170     for (size_t i = 0; i < max && n > 0; i++, n /= 2, s--) {
 171         *= (% 2) + '0';
 172     }
 173     return s + 1;
 174 }
 175 
 176 // convert_hex handles values which start as `0x`
 177 bool convert_hex(char* s) {
 178     long long n = 0;
 179     for (size_t i = 2; true; i++) {
 180         unsigned char b = s[i];
 181         if (== 0) {
 182             break;
 183         }
 184 
 185         if ('0' <= b && b <= '9') {
 186             n = 16 * n + (- '0');
 187             continue;
 188         }
 189         if ('a' <= b && b <= 'f') {
 190             n = 16 * n + (- 'a' + 10);
 191             continue;
 192         }
 193         if ('A' <= b && b <= 'F') {
 194             n = 16 * n + (- 'A' + 10);
 195             continue;
 196         }
 197 
 198         fprintf(stderr, "\x1b[41m\x1b[97minvalid hexadecimal value %s\x1b[0m\n", s);
 199         return false;
 200     }
 201 
 202     char bin[65];
 203     char* str = write_bin(bin, sizeof(bin) - 1, n);
 204     if (32 < n && n <= 126) {
 205         fprintf(stdout, "%s = 0d%lld = 0b%s = %c\n", s, n, str, (char)n);
 206     } else {
 207         fprintf(stdout, "%s = 0d%lld = 0b%s\n", s, n, str);
 208     }
 209     return true;
 210 }
 211 
 212 // convert_hex handles values which start as `0d`
 213 bool convert_dec(char* s) {
 214     // long long n = atoll(s + 2);
 215     double f = 0;
 216     if (!try_number(+ 2, &f)) {
 217         fprintf(stderr, "\x1b[41m\x1b[97minvalid number %s\x1b[0m\n", s + 2);
 218         return false;
 219     }
 220     if (< 1) {
 221         f = 0;
 222     }
 223     unsigned long long n = (unsigned long long)f;
 224 
 225     char bin[65];
 226     char* str = write_bin(bin, sizeof(bin) - 1, n);
 227     if (32 < n && n <= 126) {
 228         fprintf(stdout, "%s = 0x%llx = 0b%s = %c\n", s, n, str, (char)n);
 229     } else {
 230         fprintf(stdout, "%s = 0x%llx = 0b%s\n", s, n, str);
 231     }
 232     return true;
 233 }
 234 
 235 // convert_hex handles values which start as `0b`
 236 bool convert_bin(char* s) {
 237     long long n = 0;
 238     for (size_t i = 2; true; i++) {
 239         unsigned char b = s[i];
 240         if (== 0) {
 241             break;
 242         }
 243         if (== '0') {
 244             n *= 2;
 245         }
 246         if (== '1') {
 247             n = 2 * n + 1;
 248         }
 249 
 250         fprintf(stderr, "\x1b[41m\x1b[97minvalid binary value %s\x1b[0m\n", s);
 251         return false;
 252     }
 253 
 254     if (32 < n && n <= 126) {
 255         fprintf(stdout, "%s = 0d%lld = 0x%llx = %c\n", s, n, n, (char)n);
 256     } else {
 257         fprintf(stdout, "%s = 0d%lld = 0x%llx\n", s, n, n);
 258     }
 259     return true;
 260 }
 261 
 262 // convert_forward shows a unit-conversion from a matched table-entry index
 263 void convert_forward(double x, size_t i) {
 264     char* src = conversions[i].src;
 265     char* dst = conversions[i].dst;
 266     double mul = conversions[i].mul;
 267     double add = conversions[i].add;
 268     char* style = styles[conversions[i].kind];
 269 
 270     double y = mul * x + add;
 271     fprintf(stdout, "%s%g %-3s = %.4f %s\x1b[0m\n", style, x, src, y, dst);
 272 }
 273 
 274 // convert_backward shows a unit-conversion from a matched table-entry index
 275 void convert_backward(double x, size_t i) {
 276     char* src = conversions[i].src;
 277     char* dst = conversions[i].dst;
 278     double mul = conversions[i].mul;
 279     double add = conversions[i].add;
 280     char* style = styles[conversions[i].kind];
 281 
 282     double y = (- add) / mul;
 283     fprintf(stdout, "%s%g %-3s = %.4f %s\x1b[0m\n", style, x, dst, y, src);
 284 }
 285 
 286 // convert tries to convert the number of the unit given, showing the result:
 287 // when the unit is an empty string, all supported conversions end up showing
 288 bool convert(double x, char* src_unit) {
 289     size_t len;
 290 
 291     // no unit means convert all units in both directions
 292     len = sizeof(conversions) / sizeof(conversions[0]);
 293     if (strcmp(src_unit, "") == 0) {
 294         for (size_t i = 0; i < len; i++) {
 295             convert_forward(x, i);
 296             convert_backward(x, i);
 297         }
 298         return true;
 299     }
 300 
 301     // handle aliases for units with hard-to-type superscripts
 302     len = sizeof(aliases) / sizeof(aliases[0]);
 303     for (size_t i = 0; i < len; i++) {
 304         if (strcmpins(src_unit, aliases[i].src) == 0) {
 305             // replace unit with a matchable name
 306             src_unit = aliases[i].dst;
 307             break;
 308         }
 309     }
 310 
 311     // convert all matching table entries
 312     size_t matches = 0;
 313     len = sizeof(conversions) / sizeof(conversions[0]);
 314     for (size_t i = 0; i < len; i++) {
 315         if (strcmpins(conversions[i].src, src_unit) == 0) {
 316             convert_forward(x, i);
 317             matches++;
 318         } else if (strcmpins(conversions[i].dst, src_unit) == 0) {
 319             convert_backward(x, i);
 320             matches++;
 321         }
 322     }
 323 
 324     return matches > 0;
 325 }
 326 
 327 void unsupported_unit(char* s) {
 328     fprintf(stderr, "\x1b[41m\x1b[97munit named %s isn't supported\x1b[0m\n", s);
 329 }
 330 
 331 int run(int argc, char** argv) {
 332     if (argc < 2) {
 333         // no cmd-line args, so convert all units
 334         convert(1.0, "");
 335         // convert temperature 0s too, which may be more useful in this case
 336         convert(0.0, "F");
 337         convert(0.0, "K");
 338         convert(0.0, "C");
 339         return 0;
 340     }
 341 
 342 
 343     double value = 0;
 344     bool got_value = false;
 345     size_t errors = 0;
 346 
 347     for (size_t i = 1; i < argc; i++) {
 348         if (has_prefix(argv[i], "0x")) {
 349             if (got_value) {
 350                 convert(value, "");
 351                 got_value = false;
 352             }
 353             if (!convert_hex(argv[i])) {
 354                 errors++;
 355             }
 356             continue;
 357         }
 358 
 359         if (has_prefix(argv[i], "0d")) {
 360             if (got_value) {
 361                 convert(value, "");
 362                 got_value = false;
 363             }
 364             if (!convert_dec(argv[i])) {
 365                 errors++;
 366             }
 367             continue;
 368         }
 369 
 370         if (has_prefix(argv[i], "0b")) {
 371             if (got_value) {
 372                 convert(value, "");
 373                 got_value = false;
 374             }
 375             if (!convert_bin(argv[i])) {
 376                 errors++;
 377             }
 378             continue;
 379         }
 380 
 381         if (!got_value) {
 382             // no values, no units
 383             double n;
 384             if (try_number(argv[i], &n)) {
 385                 // a value
 386                 value = n;
 387                 got_value = true;
 388             } else {
 389                 // a unit with no value before it
 390                 if (!convert(1.0, argv[i])) {
 391                     unsupported_unit(argv[i]);
 392                     errors++;
 393                 }
 394             }
 395             continue;
 396         }
 397 
 398         // there's already a value
 399 
 400         double n;
 401         if (try_number(argv[i], &n)) {
 402             // 2 consecutive values without units
 403             convert(value, "");
 404             value = n;
 405         } else {
 406             // a value followed by its unit
 407             if (!convert(value, argv[i])) {
 408                 unsupported_unit(argv[i]);
 409                 errors++;
 410             }
 411             got_value = false;
 412         }
 413     }
 414 
 415     // don't forget trailing values
 416     if (got_value) {
 417         convert(value, "");
 418     }
 419     return 0;
 420 }
 421 
 422 int main(int argc, char** argv) {
 423 #ifdef _WIN32
 424     // ensure lines end in LF instead of CRLF on windows
 425     setmode(fileno(stdout), O_BINARY);
 426     setmode(fileno(stderr), O_BINARY);
 427 #endif
 428 
 429     // disable buffering for stdout and stderr
 430     setvbuf(stdout, NULL, _IONBF, 0);
 431     setvbuf(stderr, NULL, _IONBF, 0);
 432 
 433     return run(argc, argv);
 434 }