File: ./bytes.c
   1 #include <stdlib.h>
   2 #include "bytes.h"
   3 
   4 // new_slice is the constructor for type slice
   5 slice new_slice(size_t cap) {
   6     slice res;
   7     res.cap = cap;
   8     res.len = 0;
   9     res.ptr = malloc(res.cap);
  10     return res;
  11 }
  12 
  13 // advance updates a slice so it starts after the number of bytes given
  14 inline void advance(slice* src, size_t n) {
  15     src->ptr += n;
  16     src->len -= n;
  17 }
  18 
  19 // first creates a slice ending at the number of bytes given
  20 slice first(slice src, size_t n) {
  21     src.len = n;
  22     return src;
  23 }
  24 
  25 // append_byte does as it says, potentially reallocating the memory area
  26 // backing the slice given
  27 void append_byte(slice* s, unsigned char b) {
  28     if (s->len < s->cap) {
  29         // under capacity, so it's ok to append directly
  30         s->ptr[s->len] = b;
  31         s->len++;
  32         return;
  33     }
  34 
  35     // slice is full, so double it and reallocate
  36     s->cap *= 2;
  37     s->ptr = realloc(s->ptr, s->cap);
  38 
  39     // now append directly to the larger array
  40     s->ptr[s->len] = b;
  41     s->len++;
  42 }
  43 
  44 // find_lf returns the index of the first line-feed found, or a negative value
  45 // on failure
  46 long long int find_lf(slice s) {
  47     for (size_t i = 0; i < s.len; i++) {
  48         if (s.ptr[i] == '\n') {
  49             return i;
  50         }
  51     }
  52     return -1;
  53 }
  54 
  55 // find_digit returns the index of the first digit found, or a negative value
  56 // on failure
  57 long long int find_digit(slice s) {
  58     for (size_t i = 0; i < s.len; i++) {
  59         const unsigned char b = s.ptr[i];
  60         if ('0' <= b && b <= '9') {
  61             return i;
  62         }
  63     }
  64     return -1;
  65 }
  66 
  67 // find_non_digit returns the index of the first non-digit found, or a negative
  68 // value on failure
  69 long long int find_non_digit(slice s) {
  70     for (size_t i = 0; i < s.len; i++) {
  71         const unsigned char b = s.ptr[i];
  72         if (< '0' || b > '9') {
  73             return i;
  74         }
  75     }
  76     return -1;
  77 }

     File: ./bytes.h
   1 #ifndef _NN_BYTES_H_
   2 #define _NN_BYTES_H_
   3 
   4 #include <stddef.h>
   5 
   6 // slice is a growable region of bytes in memory
   7 typedef struct slice {
   8     // ptr is the starting place of the region
   9     unsigned char* ptr;
  10 
  11     // len is how many bytes are currently being used
  12     size_t len;
  13 
  14     // cap is how many bytes the memory region has available
  15     size_t cap;
  16 } slice;
  17 
  18 // new_slice is the constructor for type slice
  19 slice new_slice(size_t cap);
  20 
  21 // advance updates a slice so it starts after the number of bytes given
  22 void advance(slice* src, size_t n);
  23 
  24 // first creates a slice ending at the number of bytes given
  25 slice first(slice src, size_t n);
  26 
  27 // append_byte does as it says, potentially reallocating the memory area
  28 // backing the slice given
  29 void append_byte(slice* s, unsigned char b);
  30 
  31 // find_lf returns the index of the first line-feed found, or a negative value
  32 // on failure
  33 long long int find_lf(slice s);
  34 
  35 // find_digit returns the index of the first digit found, or a negative value
  36 // on failure
  37 long long int find_digit(slice s);
  38 
  39 // find_non_digit returns the index of the first non-digit found, or a negative
  40 // value on failure
  41 long long int find_non_digit(slice s);
  42 
  43 #endif

     File: ./info.txt
   1 nn [filenames...]
   2 
   3 Nice Numbers is an app which renders the plain text it's given to make long
   4 numbers much easier to read, by alternating 3-digit groups which are colored
   5 using ANSI-codes with unstyled ones.
   6 
   7 Unlike the common practice of inserting commas between 3-digit groups, this
   8 alternative doesn't widen the original text, keeping any alignments the same.
   9 
  10 All input is assumed to be UTF-8.

     File: ./io.c
   1 #include <stdbool.h>
   2 #include <stdlib.h>
   3 #include "bytes.h"
   4 #include "io.h"
   5 
   6 const unsigned char digits_style[] = "\x1b[38;5;245m";
   7 const unsigned char reset_style[] = "\x1b[0m";
   8 
   9 // new_bufreader is the constructor for type bufreader
  10 bufreader new_bufreader(FILE* src, size_t cap) {
  11     bufreader res;
  12     res.cap = cap;
  13     res.len = 0;
  14     res.pos = 0;
  15     res.src = src;
  16     res.buf = malloc(res.cap);
  17     return res;
  18 }
  19 
  20 // close_bufwriter deallocates the buffer
  21 void close_bufreader(bufreader* r) {
  22     free(r->buf);
  23     r->buf = NULL;
  24     r->len = 0;
  25 }
  26 
  27 // read_byte does as it says: check its return for the value EOF, before
  28 // using it as the next byte
  29 int read_byte(bufreader* r) {
  30     if (r->pos < r->len) {
  31         // inside current chunk
  32         const unsigned char b = r->buf[r->pos];
  33         r->pos++;
  34         return b;
  35     }
  36 
  37     // need to read the next block
  38     r->pos = 0;
  39     r->len = fread(r->buf, sizeof(unsigned char), r->cap, r->src);
  40     if (r->len > 0) {
  41         const unsigned char b = r->buf[r->pos];
  42         r->pos++;
  43         return b;
  44     }
  45 
  46     // reached the end of data
  47     return EOF;
  48 }
  49 
  50 // new_bufwriter is the constructor for type bufwriter
  51 bufwriter new_bufwriter(FILE* dst, size_t cap) {
  52     bufwriter res;
  53     res.cap = cap;
  54     res.done = false;
  55     res.len = 0;
  56     res.out = dst;
  57     res.buf = malloc(res.cap);
  58     return res;
  59 }
  60 
  61 // close_bufwriter ensures all output is shown and deallocates the buffer
  62 void close_bufwriter(bufwriter* w) {
  63     flush(w);
  64     free(w->buf);
  65     w->buf = NULL;
  66 }
  67 
  68 // flush does as it says: it empties the buffer after ensuring its bytes end
  69 // on their intended destination
  70 void flush(bufwriter* w) {
  71     if (w->len > 0 && fwrite(w->buf, w->len, 1, w->out) < 1) {
  72         w->done = true;
  73     }
  74     w->len = 0;
  75 }
  76 
  77 // write_bytes does as it says, minimizing the number of calls to fwrite
  78 void write_bytes(bufwriter* w, const unsigned char* src, size_t len) {
  79     if (w->len + len < w->cap) {
  80         // all bytes fit into buffer
  81         memcpy(w->buf + w->len, src, len);
  82         w->len += len;
  83         return;
  84     }
  85 
  86     // ensure current buffer bytes go out, before crossing strides
  87     flush(w);
  88 
  89     // emit all chunks striding beyond/at the buffer's capacity
  90     for (; len >= w->cap; src += w->cap, len -= w->cap) {
  91         if (fwrite(src, w->cap, 1, w->out) < 1) {
  92             w->done = true;
  93             return;
  94         }
  95     }
  96 
  97     // now all, if any, remaining bytes will fit into the buffer
  98     memcpy(w->buf, src, len);
  99     w->len += len;
 100 }
 101 
 102 // write_byte does as it says
 103 void write_byte(bufwriter* w, unsigned char b) {
 104     if (w->len >= w->cap) {
 105         flush(w);
 106     }
 107     w->buf[w->len] = b;
 108     w->len++;
 109 }
 110 
 111 // restyle_digits renders a run of digits as alternating styled/unstyled runs
 112 // of 3 digits, which greatly improves readability, and is the only purpose
 113 // of this app; string is assumed to be all decimal digits
 114 void restyle_digits(bufwriter* w, slice digits) {
 115     if (digits.len < 4) {
 116         // digit sequence is short, so emit it as is
 117         write_bytes(w, digits.ptr, digits.len);
 118         return;
 119     }
 120 
 121     // separate leading 0..2 digits which don't align with the 3-digit groups
 122     size_t lead = digits.len % 3;
 123     // emit leading digits unstyled, if there are any
 124     write_bytes(w, digits.ptr, lead);
 125     // the rest is guaranteed to have a length which is a multiple of 3
 126     advance(&digits, lead);
 127 
 128     // start with the alternate style, unless there were no leading digits
 129     bool style = lead != 0;
 130 
 131     while (digits.len > 0) {
 132         if (style) {
 133             write_bytes(w, digits_style, sizeof(digits_style) - 1);
 134             write_bytes(w, digits.ptr, 3);
 135             write_bytes(w, reset_style, sizeof(reset_style) - 1);
 136         } else {
 137             write_bytes(w, digits.ptr, 3);
 138         }
 139 
 140         advance(&digits, 3);
 141         // alternate between styled and unstyled 3-digit groups
 142         style = !style;
 143     }
 144 }
 145 
 146 // restyle_line renders the line given, using ANSI-styles to make any long
 147 // numbers in it more legible
 148 void restyle_line(bufwriter* w, slice line) {
 149     while (!w->done && line.len > 0) {
 150         // find where/if the next run of digits starts
 151         long int i = find_digit(line);
 152         if (>= 0) {
 153             // write non-digits which precede with run of digits
 154             write_bytes(w, line.ptr, i);
 155             advance(&line, i);
 156 
 157             // find where/if this run of digits ends
 158             long int j = find_non_digit(line);
 159             if (>= 0) {
 160                 // emit digits in groups of 3, alternating styles
 161                 restyle_digits(w, first(line, j));
 162                 advance(&line, j);
 163                 continue;
 164             }
 165 
 166             // run of digits ends with the line
 167             restyle_digits(w, line);
 168             return;
 169         }
 170 
 171         // no more digits in the rest of the line
 172         write_bytes(w, line.ptr, line.len);
 173         return;
 174     }
 175 }

     File: ./io.h
   1 #ifndef _NN_IO_H
   2 #define _NN_IO_H
   3 
   4 #include <stdbool.h>
   5 #include <stdio.h>
   6 #include <string.h>
   7 #include "bytes.h"
   8 
   9 // bufreader is a way to speed up reading data by reducing the frequency of
  10 // data reads from the a data source, while still allowing reading 1 byte at
  11 // a time
  12 typedef struct bufreader {
  13     // buf is the buffer, (re)filled periodically as needed
  14     unsigned char* buf;
  15 
  16     // len is how many buffer bytes are being used, out of its max capacity
  17     size_t len;
  18 
  19     // cap is the buffer's capacity, or the most bytes it can hold at once
  20     size_t cap;
  21 
  22     // pos is the current position, up to the current buffer length
  23     size_t pos;
  24 
  25     // src is the data source used to fill the buffer
  26     FILE* src;
  27 } bufreader;
  28 
  29 // new_bufreader is the constructor for type bufreader
  30 bufreader new_bufreader(FILE* src, size_t cap);
  31 
  32 // close_bufreader deallocates the buffer
  33 void close_bufreader(bufreader* r);
  34 
  35 // read_byte does as it says: check its return for the value EOF, before
  36 // using it as the next byte
  37 int read_byte(bufreader* r);
  38 
  39 // bufwriter is, as the name implies, a buffered-writer: when it's aimed at
  40 // stdout, it considerably speeds up this app, as intended
  41 typedef struct bufwriter {
  42     // buf is the buffer proper
  43     unsigned char* buf;
  44 
  45     // len is how many bytes of the buffer are currently being used
  46     size_t len;
  47 
  48     // cap is the capacity of the buffer, or the most bytes it can hold
  49     size_t cap;
  50 
  51     // out is the destination of all that's written into the buffer
  52     FILE* out;
  53 
  54     // done signals when/if no more output is accepted at the destination
  55     bool done;
  56 } bufwriter;
  57 
  58 // new_bufwriter is the constructor for type bufwriter
  59 bufwriter new_bufwriter(FILE* dst, size_t cap);
  60 
  61 // close_bufwriter ensures all output is shown and deallocates the buffer
  62 void close_bufwriter(bufwriter* w);
  63 
  64 // flush does as it says: it empties the buffer after ensuring its bytes end
  65 // on their intended destination
  66 void flush(bufwriter* w);
  67 
  68 // write_bytes does as it says, minimizing the number of calls to fwrite
  69 void write_bytes(bufwriter* w, const unsigned char* src, size_t len);
  70 
  71 // write_byte does as it says
  72 void write_byte(bufwriter* w, unsigned char b);
  73 
  74 // restyle_line renders the line given, using ANSI-styles to make any long
  75 // numbers in it more legible
  76 void restyle_line(bufwriter* w, slice line);
  77 
  78 #endif

     File: ./main.c
   1 #include <fcntl.h>
   2 #include <stdbool.h>
   3 #include <stddef.h>
   4 #include <stdlib.h>
   5 #include "bytes.h"
   6 #include "io.h"
   7 
   8 // handle_reader loops over input lines, restyling all digit-runs as more
   9 // readable `nice numbers`, fulfilling the app's purpose
  10 void handle_reader(bufwriter* w, FILE* src) {
  11     unsigned char prev = 0;
  12     bufreader r = new_bufreader(src, 32 * 1024);
  13     slice line = new_slice(32 * 1024);
  14 
  15     while (!w->done) {
  16         int v = read_byte(&r);
  17         if (!= EOF) {
  18             // still more bytes to go
  19             unsigned char b = v;
  20             prev = b;
  21 
  22             if (!= '\n') {
  23                 // no end of line yet
  24                 append_byte(&line, b);
  25                 continue;
  26             }
  27 
  28             // end of line
  29             append_byte(&line, b);
  30             restyle_line(w, line);
  31             line.len = 0;
  32             continue;
  33         }
  34 
  35         // input is over
  36         break;
  37     }
  38 
  39     // don't forget the last line
  40     restyle_line(w, line);
  41 
  42     // ensure last output line ends with a line-feed since, at least on
  43     // msys/windows, `less` hangs when lines with millions of symbols
  44     // don't end with a lf
  45     if (prev != '\n') {
  46         write_byte(w, '\n');
  47     }
  48 
  49     close_bufreader(&r);
  50     free(line.ptr);
  51 }
  52 
  53 // handle_file handles data from the filename given; returns false only when
  54 // the file can't be opened
  55 bool handle_file(bufwriter* w, char* fname) {
  56     FILE* f = fopen(fname, "rb");
  57     if (== NULL) {
  58         // ensure currently-buffered/deferred output shows up right now: not
  59         // doing so may scramble results in the common case where stdout and
  60         // stderr are the same, thus confusing users
  61         flush(w);
  62 
  63         fprintf(stderr, "\x1b[31mcan't open file named %s\x1b[0m\n", fname);
  64         return false;
  65     }
  66 
  67     handle_reader(w, f);
  68     fclose(f);
  69     return true;
  70 }
  71 
  72 // run returns the number of errors
  73 size_t run(int argc, char** argv) {
  74     bufwriter w = new_bufwriter(stdout, 32 * 1024);
  75 
  76     if (argc < 2) {
  77         handle_reader(&w, stdin);
  78         close_bufwriter(&w);
  79         return 0;
  80     }
  81 
  82     size_t errors = 0;
  83     for (size_t i = 1; i < argc && !w.done; i++) {
  84         if (> 1) {
  85             // put an extra empty line between adjacent outputs
  86             write_byte(&w, '\n');
  87         }
  88 
  89         if (!handle_file(&w, argv[i])) {
  90             errors++;
  91         }
  92     }
  93 
  94     close_bufwriter(&w);
  95     return errors;
  96 }
  97 
  98 int main(int argc, char** argv) {
  99 #ifdef _WIN32
 100     setmode(fileno(stdin), O_BINARY);
 101     // ensure output lines end in LF instead of CRLF on windows
 102     setmode(fileno(stdout), O_BINARY);
 103     setmode(fileno(stderr), O_BINARY);
 104 #endif
 105 
 106     // disable automatic stdio buffering, in favor of explicit buffering
 107     setvbuf(stdin, NULL, _IONBF, 0);
 108     setvbuf(stdout, NULL, _IONBF, 0);
 109     setvbuf(stderr, NULL, _IONBF, 0);
 110 
 111     return run(argc, argv) == 0 ? 0 : 1;
 112 }