├── .gitignore ├── Brewfile ├── LICENSE.md ├── Readme.md ├── deploy_site.sh ├── out └── .gitkeep ├── patcher.py ├── requirements.txt ├── site ├── .gitignore ├── 404.html ├── Gemfile ├── Gemfile.lock ├── _config.yml ├── _data │ └── versions.yml ├── downloads │ └── .gitkeep ├── fonts │ └── .gitkeep └── index.html └── test.html /.gitignore: -------------------------------------------------------------------------------- 1 | out/*.otf 2 | out/*.ttf 3 | out/*.zip 4 | out/*.woff2 5 | mods.fea 6 | fonts/* 7 | 8 | site/fonts/*.woff2 9 | site/downloads/*.zip 10 | site/_data/manifest.json 11 | -------------------------------------------------------------------------------- /Brewfile: -------------------------------------------------------------------------------- 1 | brew "fontforge" 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2019 Tristan Hume and others 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Numderline 2 | 3 | Numderline is a hacky font patcher that takes a font and converts it into one that underlines alternating groups of 3 digits starting from the right. It can also do other similar tricks. 4 | 5 | It was inspired by [my job](https://www.janestreet.com/technology/) involving a lot of staring at numbers in nanoseconds and trying to pick out the milliseconds or microseconds. 6 | 7 | A blog post about this and a web page to see and download pre-patched fonts should hopefully be coming soon™. 8 | 9 | ## Features 10 | 11 | - Patch any font to underline alternating groups of 3 digits in numbers 12 | - For use in proportional contexts, can also just insert fake commas with shaping 13 | - Alternatively it can squish digits and group them closer together in threes 14 | - Or some other variants, including small monospace mini-commas! 15 | 16 | ### Usage 17 | 18 | 1. Clone the repo 19 | 1. Install the FontForge Python API, on macOS I used Homebrew to do this with `brew install fontforge`, or you can use `brew bundle` 20 | 1. Install the fonttools API, I used `pip3 install fonttools` to install it in my Homebrew python3, or you can use `pip3 install -r requirements.txt` 21 | 1. Run `python3 patcher.py FONT_FILE_TO_PATCH` and look in the `out` folder 22 | 23 | You can also run `python3 patcher.py --help` and read the source to see other options. 24 | -------------------------------------------------------------------------------- /deploy_site.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Only works on Tristan's computer, ask him if you want the site deployed 3 | cp -r site/_site/* ~/Box/Sites/thume/numderline/ 4 | -------------------------------------------------------------------------------- /out/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trishume/numderline/e09528699a1bca52ce5e2d22a019dfdd034808ca/out/.gitkeep -------------------------------------------------------------------------------- /patcher.py: -------------------------------------------------------------------------------- 1 | # Based on https://github.com/powerline/fontpatcher/blob/develop/scripts/powerline-fontpatcher 2 | # Used under the MIT license 3 | 4 | import argparse 5 | import sys 6 | import re 7 | import subprocess 8 | import os.path 9 | import json 10 | import shutil 11 | import zipfile 12 | 13 | from itertools import chain 14 | from collections import defaultdict 15 | 16 | try: 17 | import fontforge 18 | import psMat 19 | # from fontTools.misc.py23 import * 20 | from fontTools.ttLib import TTFont 21 | from fontTools.feaLib.builder import addOpenTypeFeatures, Builder 22 | except ImportError: 23 | sys.stderr.write('The required FontForge and fonttools modules could not be loaded.\n\n') 24 | sys.stderr.write('You need FontForge with Python bindings for this script to work.\n') 25 | sys.exit(1) 26 | 27 | 28 | def get_argparser(ArgumentParser=argparse.ArgumentParser): 29 | parser = ArgumentParser( 30 | description=('Font patcher for Numderline. ' 31 | 'Requires FontForge with Python bindings. ' 32 | 'Stores the patched font as a new, renamed font file by default.') 33 | ) 34 | parser.add_argument('target_fonts', help='font files to patch', metavar='font', 35 | nargs='*', type=argparse.FileType('rb')) 36 | parser.add_argument('--group', 37 | help='group squished digits in threes, shorthand for --no-underline --shift-amount 100 --squish 0.85 --squish-all', 38 | default=False, action='store_true') 39 | parser.add_argument('--no-rename', 40 | help='don\'t add " with Numderline" to the font name', 41 | default=True, action='store_false', dest='rename_font') 42 | parser.add_argument('--no-underline', 43 | help='don\'t add underlines', 44 | default=True, action='store_false', dest='add_underlines') 45 | parser.add_argument('--no-decimals', 46 | help='don\'t touch digits after the decimal point', 47 | default=True, action='store_false', dest='do_decimals') 48 | parser.add_argument('--add-commas', 49 | help='add commas', 50 | default=False, action='store_true') 51 | parser.add_argument('--shift-amount', help='amount to shift digits to group them together, try 100', type=int, default=0) 52 | parser.add_argument('--squish', help='horizontal scale to apply to the digits to maybe make them more readable when shifted', type=float, default=1.0) 53 | parser.add_argument('--squish-all', 54 | help='squish all numbers, including decimals and ones less than 4 digits, use with --squish flag', 55 | default=False, action='store_true') 56 | parser.add_argument('--sub-font', help='substitute alternating groups of 3 with this font', type=argparse.FileType('rb')) 57 | parser.add_argument('--spaceless-commas', 58 | help='manipulate commas to not change the spacing, for monospace fonts, use with --add-commas', 59 | default=False, action='store_true') 60 | parser.add_argument('--debug-annotate', 61 | help='annotate glyph copies with debug digits', 62 | default=False, action='store_true') 63 | parser.add_argument('--build-release', 64 | help='build the default release of Numderline', 65 | default=False, action='store_true') 66 | return parser 67 | 68 | 69 | FONT_NAME_RE = re.compile(r'^([^-]*)(?:(-.*))?$') 70 | NUM_DIGIT_COPIES = 7 71 | 72 | def gen_feature(digit_names, underscore_name, dot_name, do_decimals): 73 | if do_decimals: 74 | decimal_sub = """ 75 | sub {dot_name} @digits' by @nd2; 76 | sub @nd2 @digits' by @nd1; 77 | sub @nd1 @digits' by @nd6; 78 | sub @nd6 @digits' by @nd5; 79 | sub @nd5 @digits' by @nd4; 80 | sub @nd4 @digits' by @nd3; 81 | sub @nd3 @digits' by @nd2; 82 | """ 83 | else: 84 | decimal_sub = """ 85 | ignore sub {dot_name} @digits'; 86 | sub @digits @digits' by @digits; 87 | """ 88 | 89 | decimal_sub = decimal_sub.format(dot_name=dot_name) 90 | 91 | feature = """ 92 | languagesystem DFLT dflt; 93 | languagesystem latn dflt; 94 | languagesystem cyrl dflt; 95 | languagesystem grek dflt; 96 | languagesystem kana dflt; 97 | @digits=[{digit_names}]; 98 | {nds} 99 | 100 | feature calt {{ 101 | {decimal_sub} 102 | 103 | sub @digits' @digits @digits @digits by @nd0; 104 | sub @nd0 @digits' by @nd0; 105 | 106 | reversesub @nd0' @nd0 by @nd1; 107 | reversesub @nd0' @nd1 by @nd2; 108 | reversesub @nd0' @nd2 by @nd3; 109 | reversesub @nd0' @nd3 by @nd4; 110 | reversesub @nd0' @nd4 by @nd5; 111 | reversesub @nd0' @nd5 by @nd6; 112 | reversesub @nd0' @nd6 by @nd1; 113 | }} calt; 114 | """[1:] 115 | 116 | nds = [' '.join(['nd{}.{}'.format(i,j) for j in range(10)]) for i in range(NUM_DIGIT_COPIES)] 117 | nds = ['@nd{}=[{}];'.format(i,nds[i]) for i in range(NUM_DIGIT_COPIES)] 118 | nds = "\n".join(nds) 119 | feature = feature.format(digit_names=' '.join(digit_names), 120 | nds=nds, underscore_name=underscore_name, decimal_sub=decimal_sub) 121 | with open('mods.fea', 'w') as f: 122 | f.write(feature) 123 | 124 | def shift_layer(layer, shift): 125 | layer = layer.dup() 126 | mat = psMat.translate(shift, 0) 127 | layer.transform(mat) 128 | return layer 129 | 130 | def squish_layer(layer, squish): 131 | layer = layer.dup() 132 | mat = psMat.scale(squish, 1.0) 133 | layer.transform(mat) 134 | return layer 135 | 136 | def add_comma_to(glyph, comma_glyph, spaceless): 137 | comma_layer = comma_glyph.layers[1].dup() 138 | x_shift = glyph.width 139 | y_shift = 0 140 | if spaceless: 141 | mat = psMat.scale(0.8, 0.8) 142 | comma_layer.transform(mat) 143 | x_shift -= comma_glyph.width / 2 144 | # y_shift = -200 145 | mat = psMat.translate(x_shift, y_shift) 146 | comma_layer.transform(mat) 147 | glyph.layers[1] += comma_layer 148 | if not spaceless: 149 | glyph.width += comma_glyph.width 150 | 151 | def annotate_glyph(glyph, extra_glyph): 152 | layer = extra_glyph.layers[1].dup() 153 | mat = psMat.translate(-(extra_glyph.width/2), 0) 154 | layer.transform(mat) 155 | mat = psMat.scale(0.3, 0.3) 156 | layer.transform(mat) 157 | mat = psMat.translate((extra_glyph.width/2), 0) 158 | layer.transform(mat) 159 | mat = psMat.translate(0, -600) 160 | layer.transform(mat) 161 | glyph.layers[1] += layer 162 | 163 | def out_path(name): 164 | return 'out/{0}.ttf'.format(name) 165 | 166 | def patch_one_font(font, rename_font, add_underlines, shift_amount, squish, squish_all, add_commas, spaceless_commas, debug_annotate, do_decimals, group, sub_font): 167 | font.encoding = 'ISO10646' 168 | 169 | if group: 170 | add_underlines = False 171 | shift_amount = 100 172 | squish = 0.85 173 | squish_all = True 174 | 175 | mod_name = 'N' 176 | if add_commas: 177 | if spaceless_commas: 178 | mod_name += 'onoCommas' 179 | else: 180 | mod_name += 'ommas' 181 | if add_underlines: 182 | mod_name += 'umderline' 183 | if sub_font is not None: 184 | mod_name += 'Sub' 185 | # Cleaner name for what I expect to be a common combination 186 | if shift_amount == 100 and squish == 0.85 and squish_all: 187 | mod_name += 'Group' 188 | else: 189 | if shift_amount != 0: 190 | mod_name += 'Shift{}'.format(shift_amount) 191 | if squish != 1.0: 192 | squish_s = '{}'.format(squish) 193 | mod_name += 'Squish{}'.format(squish_s.replace('.','p')) 194 | if squish_all: 195 | mod_name += 'All' 196 | if debug_annotate: 197 | mod_name += 'Debug' 198 | if not do_decimals: 199 | mod_name += 'NoDecimals' 200 | 201 | # Rename font 202 | if rename_font: 203 | font.familyname += ' with '+mod_name 204 | font.fullname += ' with '+mod_name 205 | fontname, style = FONT_NAME_RE.match(font.fontname).groups() 206 | font.fontname = fontname + 'With' + mod_name 207 | if style is not None: 208 | font.fontname += style 209 | font.appendSFNTName( 210 | 'English (US)', 'Preferred Family', font.familyname) 211 | font.appendSFNTName( 212 | 'English (US)', 'Compatible Full', font.fullname) 213 | 214 | digit_names = [font[code].glyphname for code in range(ord('0'),ord('9')+1)] 215 | test_names = [font[code].glyphname for code in range(ord('A'),ord('J')+1)] 216 | underscore_name = font[ord('_')].glyphname 217 | dot_name = font[ord('.')].glyphname 218 | # print(digit_names) 219 | 220 | if sub_font is not None: 221 | sub_font = fontforge.open(sub_font.name) 222 | 223 | underscore_layer = font[underscore_name].layers[1] 224 | 225 | # 0xE900 starts an area spanning until 0xF000 that as far as I can tell nothing 226 | # popular uses. I checked the Apple glyph browser and Nerd Font. 227 | # Uses an array because of python closure capture semantics 228 | encoding_alloc = [0xE900] 229 | def make_copy(src_font, loc, to_name, add_underscore, add_comma, shift, squish, annotate_with): 230 | encoding = encoding_alloc[0] 231 | src_font.selection.select(loc) 232 | src_font.copy() 233 | font.selection.select(encoding) 234 | font.paste() 235 | glyph = font[encoding] 236 | glyph.glyphname = to_name 237 | if squish != 1.0: 238 | glyph.layers[1] = squish_layer(glyph.layers[1], squish) 239 | if shift != 0: 240 | glyph.layers[1] = shift_layer(glyph.layers[1], shift) 241 | if add_underscore: 242 | glyph.layers[1] += underscore_layer 243 | if add_comma: 244 | add_comma_to(glyph, font[ord(',')], spaceless_commas) 245 | if annotate_with is not None: 246 | annotate_glyph(glyph, annotate_with) 247 | encoding_alloc[0] += 1 248 | 249 | for copy_i in range(0,NUM_DIGIT_COPIES): 250 | for digit_i in range(0,10): 251 | shift = 0 252 | if copy_i % 3 == 0: 253 | shift = -shift_amount 254 | elif copy_i % 3 == 2: 255 | shift = shift_amount 256 | in_alternating_group = (copy_i >= 3 and copy_i < 6) 257 | add_underscore = add_underlines and in_alternating_group 258 | add_comma = add_commas and (copy_i == 3 or copy_i == 6) 259 | annotate_with = font[digit_names[copy_i]] if debug_annotate else None 260 | use_sub_font = (sub_font is not None) and in_alternating_group 261 | src_font = sub_font if use_sub_font else font 262 | make_copy(src_font, digit_names[digit_i], 'nd{}.{}'.format(copy_i,digit_i), add_underscore, add_comma, shift, squish, annotate_with) 263 | 264 | if squish_all and squish != 1.0: 265 | for digit in digit_names: 266 | glyph = font[digit] 267 | glyph.layers[1] = squish_layer(glyph.layers[1], squish) 268 | 269 | gen_feature(digit_names, underscore_name, dot_name, do_decimals) 270 | 271 | font.generate('out/tmp.ttf') 272 | ft_font = TTFont('out/tmp.ttf') 273 | addOpenTypeFeatures(ft_font, 'mods.fea', tables=['GSUB']) 274 | # replacement to comply with SIL Open Font License 275 | out_name = font.fullname.replace('Source ', 'Sauce ') 276 | ft_font.save(out_path(out_name)) 277 | print("> Created '{}'".format(out_name)) 278 | 279 | if sub_font is not None: 280 | sub_font.close() 281 | 282 | return out_name 283 | 284 | 285 | def patch_fonts(target_files, *args): 286 | res = None 287 | for target_file in target_files: 288 | target_font = fontforge.open(target_file.name) 289 | try: 290 | res = patch_one_font(target_font, *args) 291 | finally: 292 | target_font.close() 293 | return res 294 | 295 | def source_font_path(weight, is_italic): 296 | suffix = weight 297 | if is_italic and weight == 'Regular': 298 | suffix = '' 299 | if is_italic: 300 | suffix += 'It' 301 | return "fonts/source-code-pro/SourceCodePro-{}.ttf".format(suffix) 302 | 303 | def build_release(): 304 | infos = { 305 | 'DejaVuSansMono' : {'prefix': 'fonts/dejavu/DejaVuSansMono', 'suffixes': ['', '-Bold', '-Oblique', '-BoldOblique']}, 306 | 'DejaVuSans' : {'prefix': 'fonts/dejavu/DejaVuSans', 'suffixes': ['', '-Bold', '-Oblique', '-BoldOblique']}, 307 | 'UbuntuMono' : {'prefix': 'fonts/ubuntu/UbuntuMono', 'suffixes': ['-R', '-B', '-RI', '-BI']}, 308 | 'Hack' : {'prefix': 'fonts/hack/Hack', 'suffixes': ['-Regular', '-Bold', '-Italic', '-BoldItalic']}, 309 | 'SauceCode' : {'prefix': 'fonts/source-code-pro/SourceCodePro', 'suffixes': ['-Regular', '-Light', '-Bold', '-It', '-BoldIt']}, 310 | } 311 | 312 | to_build = [ 313 | ('debug', 'DejaVuSansMono', ['fonts/dejavu/DejaVuSansMono.ttf', '--no-underline', '--debug-annotate']) 314 | # ['fonts/source-code-pro/SourceCodePro-Light.ttf', '--no-underline', '--sub-font', 'fonts/source-code-pro/SourceCodePro-Regular.ttf'] 315 | ] 316 | 317 | def for_all_weights(name, set_name, opts): 318 | info = infos[name] 319 | for suffix in info['suffixes']: 320 | to_build.append((set_name, name, ['{}{}.ttf'.format(info['prefix'], suffix)]+opts)) 321 | 322 | for name in ['DejaVuSansMono', 'UbuntuMono', 'SauceCode']: 323 | for_all_weights(name, 'numderline', []) 324 | 325 | for_all_weights('DejaVuSans', 'nommas', ['--no-underline', '--add-commas', '--no-decimals']) 326 | 327 | all_monospace = ['DejaVuSansMono', 'UbuntuMono', 'Hack', 'SauceCode'] 328 | for name in all_monospace: 329 | for_all_weights(name, 'ngroup', ['--no-underline', '--group']) 330 | for name in all_monospace: 331 | for_all_weights(name, 'monocommas', ['--no-underline', '--group', '--add-commas', '--spaceless-commas', '--no-decimals']) 332 | 333 | source_sub_series = ['Regular', 'Semibold', 'Bold', 'Black'] 334 | for i, weight in enumerate(source_sub_series[:-1]): 335 | for it in [False, True]: 336 | bolder = source_sub_series[i+1] 337 | to_build.append(('nbold','SauceCode',[source_font_path(weight, it), '--no-underline', '--sub-font', source_font_path(bolder, it)])) 338 | 339 | 340 | manifest = defaultdict(lambda: defaultdict(lambda: [])) 341 | for set_name, font_id, args in to_build: 342 | out_name = main(args) 343 | if out_name is None: 344 | raise Exception("Build failed") 345 | manifest[set_name][font_id].append(out_name) 346 | 347 | print(manifest) 348 | 349 | for variant, fonts in manifest.items(): 350 | for font, weights in fonts.items(): 351 | archive_name = "{}-{}".format(font, variant) 352 | with zipfile.ZipFile("site/downloads/{}.zip".format(archive_name), 'w', compression=zipfile.ZIP_DEFLATED) as myzip: 353 | for weight in weights: 354 | myzip.write(out_path(weight), '{}/{}.ttf'.format(archive_name, weight)) 355 | 356 | with open('site/_data/manifest.json','w') as f: 357 | f.write(json.dumps(manifest)) 358 | 359 | for fonts in manifest.values(): 360 | for weights in fonts.values(): 361 | subprocess.run(['woff2_compress',out_path(weights[0])]) 362 | shutil.copyfile("out/{}.woff2".format(weights[0]), "site/fonts/{}.woff2".format(weights[0])) 363 | 364 | 365 | def main(argv): 366 | args = get_argparser().parse_args(argv) 367 | 368 | if args.build_release: 369 | build_release() 370 | return 371 | 372 | return patch_fonts(args.target_fonts, args.rename_font, args.add_underlines, args.shift_amount, args.squish, args.squish_all, 373 | args.add_commas, args.spaceless_commas, args.debug_annotate, args.do_decimals, args.group, args.sub_font) 374 | 375 | 376 | main(sys.argv[1:]) 377 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | fonttools>4.0,<4.1 2 | -------------------------------------------------------------------------------- /site/.gitignore: -------------------------------------------------------------------------------- 1 | _site 2 | .sass-cache 3 | .jekyll-cache 4 | .jekyll-metadata 5 | vendor 6 | -------------------------------------------------------------------------------- /site/404.html: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: /404.html 3 | layout: default 4 | --- 5 | 6 | 19 | 20 |
Page not found :(
24 |The requested page could not be found.
25 |103 | Numderline is a font patcher that uses OpenType font shaping trickery to make it easier to visually parse large numbers. It has multiple variants for different preferences, fonts and contexts. 104 |
105 |106 | I (Tristan Hume) was inspired to build this by my job at Jane Street involving staring at a lot of large numbers, mostly latencies in nanoseconds, and wanting to pick out larger quantities like milliseconds and microseconds. 107 |
108 |109 | See the code on Github to contribute or patch fonts of your choice. 110 |
111 | {% for version in site.data.versions %} 112 |{{ version.desc }}
114 |69 | the quick brown fox jumps over the lazy dog 70 | this <= that && why == zeven === five >>= lol 71 | hamburgefont 72 | 1 73 | 12 74 | 123 75 | 1234 76 | 12345 77 | 123456 78 | 1234567 79 | 12345678 80 | 123456789 81 | 1234567890 82 | 12345678901 83 | 123456789012 84 | 1234567890123 85 | 12345678901234 86 | 123456789012345 87 | 1234567890123456 88 | 12345678901234567 89 | 123456789012345678 90 | 1234567890123456789 91 | 12345678901234567890 92 | 93 | 1234.1 94 | 1234.12 95 | 1234.123 96 | 1234.1234 97 | 1234.12345 98 | 1234.123456 99 | 1234.1234567 100 | 1234.12345678 101 | 1234.123456789 102 | 1234.1234567890 103 | 104 | 111111 105 | 222222 106 | 777777 107 | 108 | 123456 12345678 12345678 109 | 110 | aoeu1234125otnuehnth 111 | 12543,1243152,12543523112 |
113 | the quick brown fox jumps over the lazy dog 114 | 1234567890 123 115 | 1234567 123423,124356.0000000116 |
117 | the quick brown fox jumps over the lazy dog 118 | 1234567890 123 119 | 1234567 123423,124356.0000000120 |
121 | the quick brown fox jumps over the lazy dog 122 | 1234567890 123 123 | 1234567 123423,124356.0000000124 |
Benchmark | Nanoseconds |
---|---|
blah_foo_bar | 17892384923 |
blah_baz1 | 16892323 |
blah_baz2 | 14772243 |
blah_baz3 | 16423444 |
blah_baz4 | 16978123 |
blah_qux | 17923 |
blah_lorem | 234923 |
135 | time system flow_type latency_ns epoch ack_latency_ns transit_delay_ns 136 | ------------------------- ---------- ---------- ---------- ---------------- -------------- ---------------- 137 | 2019-10-02 22:11:59 -0400 test Order 31439 1570068719.15762 814364 9045303407 138 | 2019-10-02 22:11:59 -0400 test Cancel 65354 1570068719.15764 863851 8572002092 139 | 2019-10-02 22:11:59 -0400 test Cancel 74405 1570068719.15765 837488 2089369225 140 | 2019-10-02 22:11:59 -0400 test Cancel 5279 1570068719.15767 223318 5853333820 141 | 2019-10-02 22:11:59 -0400 test Cancel 88937 1570068719.15768 49391 31165277747 142 | 2019-10-02 22:11:59 -0400 test Cancel 17868 1570068719.15769 456572 11704916307 143 | 2019-10-02 22:11:59 -0400 test Cancel 6392 1570068719.15771 733550 38885924077 144 | 2019-10-02 22:11:59 -0400 test Order 18447 1570068719.15772 407225 43493701983 145 | 2019-10-02 22:11:59 -0400 test Cancel 4202 1570068719.15773 66013 33465308851 146 | 2019-10-02 22:11:59 -0400 test Cancel 49780 1570068719.15774 516106 16016644531 147 | 2019-10-02 22:11:59 -0400 test Order 97926 1570068719.15775 265912 19644235826 148 | 2019-10-02 22:11:59 -0400 test Cancel 19296 1570068719.15777 10634 15924194457 149 | 2019-10-02 22:11:59 -0400 test Order 12823 1570068719.15778 535120 23514150184 150 | 2019-10-02 22:11:59 -0400 test Cancel 68016 1570068719.15779 67837 47286090933 151 | 2019-10-02 22:11:59 -0400 test Cancel 78940 1570068719.15780 602530 27100187791 152 | 2019-10-02 22:11:59 -0400 test Cancel 35906 1570068719.15782 803041 39733642501 153 | 2019-10-02 22:11:59 -0400 test Cancel 55789 1570068719.15783 989714 40888487353 154 | 2019-10-02 22:11:59 -0400 test Cancel 50383 1570068719.15784 862812 2585391372 155 | 2019-10-02 22:11:59 -0400 test Cancel 288 1570068719.15785 826173 6026064740 156 | 2019-10-02 22:11:59 -0400 test Order 89229 1570068719.15786 562273 3911931282 157 | 2019-10-02 22:11:59 -0400 test Order 12094 1570068719.15788 871769 32250236519 158 | 2019-10-02 22:11:59 -0400 test Order 32439 1570068719.15789 291946 7990450518 159 | 2019-10-02 22:11:59 -0400 test Order 65752 1570068719.15790 677662 48878932453 160 | 2019-10-02 22:11:59 -0400 test Cancel 73670 1570068719.15791 456673 41427748520 161 | 2019-10-02 22:11:59 -0400 test Cancel 56356 1570068719.15792 613750 14742523916 162 | 2019-10-02 22:11:59 -0400 test Order 76657 1570068719.15794 594455 26118260407 163 | 2019-10-02 22:11:59 -0400 test Order 51382 1570068719.15795 766570 48838516772 164 | 2019-10-02 22:11:59 -0400 test Order 46687 1570068719.15796 332451 43413161771 165 | 2019-10-02 22:11:59 -0400 test Cancel 67096 1570068719.15797 25554 33673299015 166 | 2019-10-02 22:11:59 -0400 test Cancel 11056 1570068719.15798 239809 38505070571 167 | 2019-10-02 22:11:59 -0400 test Order 27827 1570068719.15800 736729 21898159224 168 | 2019-10-02 22:11:59 -0400 test Cancel 97811 1570068719.15801 632739 30448521554 169 | 2019-10-02 22:11:59 -0400 test Order 10496 1570068719.15802 97349 40958374404 170 | 2019-10-02 22:11:59 -0400 test Order 35046 1570068719.15803 241790 9916338303 171 | 2019-10-02 22:11:59 -0400 test Order 96075 1570068719.15804 678424 24873810307 172 | 2019-10-02 22:11:59 -0400 test Order 17361 1570068719.15806 126723 10457495989 173 | 2019-10-02 22:11:59 -0400 test Order 35683 1570068719.15807 989790 8931144993 174 | 2019-10-02 22:11:59 -0400 test Order 21781 1570068719.15808 472297 14425326102 175 | 2019-10-02 22:11:59 -0400 test Cancel 49254 1570068719.15809 965808 26247909055 176 | 2019-10-02 22:11:59 -0400 test Cancel 56166 1570068719.15810 664022 48883316442 177 | 2019-10-02 22:11:59 -0400 test Cancel 41950 1570068719.15812 383750 36802874991 178 | 2019-10-02 22:11:59 -0400 test Order 93157 1570068719.15813 732659 46854420709 179 | 2019-10-02 22:11:59 -0400 test Order 8183 1570068719.15819 778452 16661995101 180 | 2019-10-02 22:11:59 -0400 test Cancel 57488 1570068719.15821 887350 12134193056 181 | 2019-10-02 22:11:59 -0400 test Order 29980 1570068719.15823 517741 36356083776 182 | 2019-10-02 22:11:59 -0400 test Order 5203 1570068719.1583 23081 16638430992 183 | 2019-10-02 22:11:59 -0400 test Order 54527 1570068719.15832 296844 39210574584 184 | 2019-10-02 22:11:59 -0400 test Order 38379 1570068719.15833 449316 25962337037 185 | 2019-10-02 22:11:59 -0400 test Order 57342 1570068719.15835 950924 404072538 186 | 2019-10-02 22:11:59 -0400 test Cancel 35902 1570068719.15836 411195 29564962362 187 |188 | 189 | 190 | --------------------------------------------------------------------------------