1 // Written in the D programming language. 2 /** 3 Haystack zinc encode. 4 5 Copyright: Copyright (c) 2017, Radu Racariu <radu.racariu@gmail.com> 6 License: $(LINK2 www.boost.org/LICENSE_1_0.txt, Boost License 1.0) 7 Authors: Radu Racariu 8 **/ 9 module haystack.zinc.encode; 10 11 import haystack.tag; 12 import std.traits : isSomeChar; 13 import std.range.primitives : isOutputRange; 14 15 /////////////////////////////////////////////////////////////////// 16 // 17 // Tag Encoding to Zinc 18 // 19 /////////////////////////////////////////////////////////////////// 20 21 /** 22 Encodes Marker as 'M'. 23 Expects an OutputRange as writer. 24 Returns: the writter OutputRange 25 */ 26 void encode(R) (auto ref const(Marker), auto ref R writer) 27 if (isOutputRange!(R, char)) 28 { 29 writer.put('M'); 30 } 31 unittest 32 { 33 assert(zinc(Marker()) != ""); 34 assert(zinc(Marker()) == "M"); 35 } 36 /** 37 Encodes Na as 'NA'. 38 Expects an OutputRange as writer. 39 Returns: the writter OutputRange 40 */ 41 void encode(R) (auto ref const(Na), auto ref R writer) 42 if (isOutputRange!(R, char)) 43 { 44 writer.put("NA"); 45 } 46 unittest 47 { 48 assert(zinc(Na()) == "NA"); 49 assert(Na().zinc() != ""); 50 } 51 /** 52 Encodes Bool as 'T' or 'F'. 53 Expects an OutputRange as writer. 54 Returns: the writter OutputRange 55 */ 56 void encode(R) (auto ref const(Bool) val, auto ref R writer) 57 if (isOutputRange!(R, char)) 58 { 59 writer.put(val ? 'T' : 'F'); 60 61 } 62 unittest 63 { 64 assert(zinc(Bool(true)) == "T"); 65 assert(zinc(Bool(false)) == "F"); 66 assert(zinc(Bool(true)) != ""); 67 } 68 /** 69 Encodes Num as 1, -34, 10_000, 5.4e-45, 9.23kg, 74.2°F, 4min, INF, -INF, NaN. 70 Expects an OutputRange as writer. 71 Returns: the writter OutputRange 72 */ 73 void encode(R) (auto ref const(Num) value, auto ref R writer) 74 if (isOutputRange!(R, char)) 75 { 76 import std.math : isInfinity, isNaN; 77 import std.format : formattedWrite; 78 79 if (isInfinity(cast(double)value)) 80 { 81 if (value < 0) 82 writer.put('-'); 83 writer.put("INF"); 84 } 85 else if (isNaN(cast(double)value)) 86 { 87 writer.put("NaN"); 88 } 89 else 90 { 91 formattedWrite(&writer, "%g", value.val); 92 if (value.unit.length) 93 writer.put(value.unit); 94 } 95 } 96 97 unittest 98 { 99 assert(Num(123).zinc() == "123"); 100 assert(Num(123.4, "s").zinc() == "123.4s"); 101 assert(Num(1.5e5, "$").zinc() == "150000$"); 102 assert(Num(1.5e5, "$").zinc() == "150000$"); 103 assert(Num(1.5e-3, "$").zinc() == "0.0015$"); 104 assert(Num(-9.9).zinc() == "-9.9"); 105 assert(Num(double.infinity).zinc() == "INF"); 106 assert(Num(-1 * double.infinity).zinc() == "-INF"); 107 assert(Num(double.nan).zinc() == "NaN"); 108 } 109 110 /** 111 Encodes Str as "hello", "foo\nbar\"". 112 Expects an OutputRange as writer. 113 Returns: the writter OutputRange 114 */ 115 void encode(R) (auto ref const(Str) val, auto ref R writer) 116 if (isOutputRange!(R, char)) 117 { 118 import std.format : formattedWrite; 119 120 writer.put(`"`); 121 foreach (dchar c; val) 122 { 123 if (c < ' ' || c == '"' || c == '\\') 124 { 125 switch (c) 126 { 127 case '"': writer.put(`\"`); break; 128 case '\\': writer.put(`\\`); break; 129 case '\b': writer.put(`\b`); break; 130 case '\f': writer.put(`\f`); break; 131 case '\n': writer.put(`\n`); break; 132 case '\r': writer.put(`\r`); break; 133 case '\t': writer.put(`\t`); break; 134 default: 135 writer.put(`\u`); 136 formattedWrite(&writer, "%04x", c); 137 } 138 } 139 else if (c == '$') 140 { 141 writer.put(`\$`); 142 } 143 else 144 { 145 writer.put(c); 146 } 147 } 148 writer.put(`"`); 149 } 150 unittest 151 { 152 assert(zinc(Str("abc\n")) == `"abc\n"`); 153 assert(zinc(Str("a\nb\tfoo")) == `"a\nb\tfoo"`); 154 assert(zinc(Str("\u0bae")) == `"ம"`); 155 assert(zinc(Str("\\\"")) == `"\\\""`); 156 assert(zinc(Str("$")) == `"\$"`); 157 assert(zinc(Str("_ \\ \" \n \r \t \u0011 _")) == `"_ \\ \" \n \r \t \u0011 _"`); 158 assert(zinc(Str("unicode char ש")) == `"unicode char ש"`); 159 } 160 /** 161 Encodes Coord as C(74.0000, -77.000) 162 Expects an OutputRange as writer. 163 Returns: the writter OutputRange 164 */ 165 void encode(R) (auto ref const(Coord) val, auto ref R writer) 166 if (isOutputRange!(R, char)) 167 { 168 import std.format : formattedWrite; 169 writer.put(`C(`); 170 formattedWrite(&writer, "%.6f", val.lat); 171 writer.put(','); 172 formattedWrite(&writer, "%.6f", val.lng); 173 writer.put(')'); 174 175 } 176 unittest 177 { 178 assert(Coord(37.545826,-77.449188).zinc() == "C(37.545826,-77.449188)"); 179 } 180 181 /** 182 Encodes XStr as Type("value"). 183 Expects an OutputRange as writer. 184 Returns: the writter OutputRange 185 */ 186 void encode(R) (auto ref const(XStr) val, auto ref R writer) 187 if (isOutputRange!(R, char)) 188 { 189 writer.put(val.type); 190 writer.put(`("`); 191 writer.put(val.val); 192 writer.put(`")`); 193 194 } 195 unittest 196 { 197 assert(XStr("Blob", "1-2").zinc() == `Blob("1-2")`); 198 } 199 /** 200 Encodes Uri as `/a/b/c`. 201 Expects an OutputRange as writer. 202 Returns: the writter OutputRange 203 */ 204 void encode(R) (auto ref const(Uri) val, auto ref R writer) 205 if (isOutputRange!(R, char)) 206 { 207 writer.put("`"); 208 foreach (size_t i, dchar c; val.val) 209 { 210 if (c < ' ') 211 continue; 212 switch(c) 213 { 214 case '`' : writer.put("\\`"); break; 215 case '\\' : writer.put(`\\`); break; 216 default: 217 if (c >= ' ' && c < 127) 218 { 219 writer.put(c); 220 } 221 else 222 { 223 writer.put(`\u`); 224 import std.format : formattedWrite; 225 formattedWrite(&writer, "%04x", c); 226 } 227 } 228 } 229 writer.put('`'); 230 231 } 232 unittest 233 { 234 assert(zinc(Uri(`/a/b/c`)) == "`/a/b/c`"); 235 } 236 /** 237 Encodes Ref as @someRef. 238 Expects an OutputRange as writer. 239 Returns: the writter OutputRange 240 */ 241 void encode(R) (auto ref const(Ref) val, auto ref R writer) 242 if (isOutputRange!(R, char)) 243 { 244 writer.put('@'); 245 writer.put(val.val); 246 247 } 248 unittest 249 { 250 assert(zinc(Ref("aa-bbc")) == "@aa-bbc"); 251 } 252 /** 253 Encodes Date as 2016-12-07 (YYYY-MM-DD). 254 Expects an OutputRange as writer. 255 Returns: the writter OutputRange 256 */ 257 void encode(R) (auto ref const(Date) val, auto ref R writer) 258 if (isOutputRange!(R, char)) 259 { 260 import std.format : formattedWrite; 261 formattedWrite(&writer, "%02d-%02d-%02d", val.year, val.month, val.day); 262 } 263 unittest 264 { 265 assert(zinc(Date(2016, 12, 7)) == "2016-12-07"); 266 } 267 /** 268 Encodes Time as 08:43:44 (hh:mm:ss.FFF). 269 Expects an OutputRange as writer. 270 Returns: the writter OutputRange 271 */ 272 void encode(R) (auto ref const(TimeOfDay) val, auto ref R writer) 273 if (isOutputRange!(R, char)) 274 { 275 import std.format : formattedWrite; 276 formattedWrite(&writer, "%02d:%02d:%02d", val.hour, val.minute, val.second); 277 278 } 279 /// ditto 280 void encode(R) (auto ref const(Time) val, auto ref R writer) 281 if (isOutputRange!(R, char)) 282 { 283 import std.format : formattedWrite; 284 encode(cast(TimeOfDay)val, writer); 285 if (val.millis > 0) 286 formattedWrite(&writer, ".%03d", val.millis); 287 288 } 289 unittest 290 { 291 assert(zinc(TimeOfDay(8, 43, 44)) == "08:43:44"); 292 assert(zinc(Time(8, 43, 44)) == "08:43:44"); 293 assert(zinc(Time(23, 59, 59, 999)) == "23:59:59.999"); 294 } 295 296 /** 297 Encodes DateTime as 2009-11-09T15:39:00Z. 298 Expects an OutputRange as writer. 299 Returns: the writter OutputRange 300 */ 301 void encode(R) (auto ref const(DateTime) val, auto ref R writer) 302 if (isOutputRange!(R, char)) 303 { 304 encode(val.date, writer); 305 writer.put('T'); 306 encode(val.timeOfDay, writer); 307 writer.put('Z'); 308 } 309 /** 310 Encodes SysTime as 2016-13-07T08:56:00-05:00 New_York. 311 Expects an OutputRange as writer. 312 Returns: the writter OutputRange 313 */ 314 void encode(R) (auto ref const(SysTime) val, auto ref R writer) 315 if (isOutputRange!(R, char)) 316 { 317 import core.time; 318 import std.datetime : UTC; 319 import std.format : formattedWrite; 320 import haystack.util.tzdata; 321 322 encode(cast(Date)val, writer); 323 writer.put('T'); 324 encode(cast(TimeOfDay)val, writer); 325 if (val.fracSecs.total!"nsecs" > 0) 326 formattedWrite(&writer, ".%03d", val.fracSecs.total!"msecs"); 327 if (val.timezone == UTC() || getTimeZoneName(val.timezone) == "UTC") 328 { 329 writer.put('Z'); 330 } 331 else 332 { 333 import std.math : abs; 334 auto offset = val.utcOffset.split!("hours", "minutes")(); 335 formattedWrite(&writer, "%s%02d:%02d", offset.hours > 0 ? "+" : "-", offset.hours.abs, offset.minutes); 336 string tzname = getTimeZoneName(val.timezone); 337 assert(tzname.length, "Time zone name can't be empty." ~ val.timezone.stdName); 338 writer.put(' '); 339 writer.put(tzname); 340 } 341 } 342 unittest 343 { 344 import core.time; 345 import haystack.util.tzdata; 346 assert(zinc(DateTime(Date(2016, 12, 7), Time(8, 56, 00))) == "2016-12-07T08:56:00Z"); 347 auto e = SysTime(DateTime(Date(2016, 12, 7), Time(8, 56, 00)), 100.msecs, timeZone("GMT+7")).zinc(); 348 string tzname = getTimeZoneName(timeZone("GMT+7")); 349 assert(e == "2016-12-07T08:56:00.100-07:00 " ~ tzname); 350 e = SysTime(DateTime(Date(2016, 12, 7), Time(8, 56, 00)), 100.msecs, timeZone("GMT-7")).zinc(); 351 tzname = getTimeZoneName(timeZone("GMT-7")); 352 assert(e == "2016-12-07T08:56:00.100+07:00 " ~ tzname); 353 } 354 /** 355 Encodes any Tag as zinc. 356 Expects an OutputRange as writer. 357 Returns: the writter OutputRange 358 */ 359 void encode(R) (auto ref const(Tag) val, auto ref R writer, SortedKeys sorted = SortedKeys.no) 360 if (isOutputRange!(R, char)) 361 { 362 import std.traits : fullyQualifiedName, moduleName; 363 364 // null value 365 if (!val.hasValue) 366 { 367 writer.put('N'); 368 return; 369 } 370 immutable isGrid = val.hasValue!Grid; 371 if (isGrid) 372 writer.put("<<\n"); 373 Tag value = cast(Tag) val; 374 // encode current value 375 value.visit!( 376 (ref Marker v) => v.encode(writer), 377 (ref Na v) => v.encode(writer), 378 (ref Bool v) => v.encode(writer), 379 (ref Num v) => v.encode(writer), 380 (ref Str v) => v.encode(writer), 381 (ref Coord v) => v.encode(writer), 382 (ref XStr v) => v.encode(writer), 383 (ref Uri v) => v.encode(writer), 384 (ref Ref v) => v.encode(writer), 385 (ref Date v) => v.encode(writer), 386 (ref Time v) => v.encode(writer), 387 (ref SysTime v) => v.encode(writer), 388 (ref List v) => v.encode(writer), 389 (ref Dict v) => v.encode(writer, DictBraces.yes, sorted), 390 (ref Grid v) => v.encode(writer, sorted) 391 )(); 392 393 if (isGrid) 394 writer.put(">>"); 395 396 } 397 unittest 398 { 399 assert(Tag().zinc() == "N"); 400 assert("foo bar".tag.zinc() == `"foo bar"`); 401 } 402 403 /** 404 Encodes TagList as [1, 2, 3]. 405 Expects an OutputRange as writer. 406 Returns: the writter OutputRange 407 */ 408 void encode(R) (const(List) val, auto ref R writer) 409 if (isOutputRange!(R, char)) 410 { 411 writer.put('['); 412 foreach (size_t i, ref tag; val) 413 { 414 tag.encode(writer); 415 if (i < val.length - 1) 416 writer.put(','); 417 } 418 writer.put(']'); 419 420 } 421 unittest 422 { 423 auto list = [1.tag, "foo".tag, false.tag]; 424 assert(list.zinc() == `[1,"foo",F]`); 425 } 426 427 /// Flag for sorting columns names and dictionary keys alphabetically 428 enum SortedKeys { no, yes } 429 430 /// Flag for wrapping zinc dicts in `{}` 431 enum DictBraces { no, yes } 432 433 /** 434 Encodes Dict as {dis:"Building" site area:35000ft²}. 435 Expects an OutputRange as writer. 436 Returns: the writter OutputRange 437 */ 438 void encode(R) (auto ref const(Dict) dict, 439 auto ref R writer, 440 DictBraces useBraces = DictBraces.yes, 441 SortedKeys sorted = SortedKeys.no) 442 if (isOutputRange!(R, char)) 443 { 444 if (useBraces == DictBraces.yes) 445 writer.put('{'); 446 447 void encodeEntry(string name, ref const(Tag) value) 448 { 449 writer.put(name); 450 if (!value.hasValue!Marker) 451 { 452 writer.put(':'); 453 value.encode(writer, sorted); 454 } 455 } 456 457 size_t i = 0; 458 if (sorted == SortedKeys.no) 459 { 460 foreach (name, ref value; dict) 461 { 462 encodeEntry(name, value); 463 if (i++ < dict.length - 1) 464 writer.put(' '); 465 } 466 } 467 else 468 { 469 import std.algorithm : sort; 470 foreach (key; dict.keys().sort()) 471 { 472 const val = dict[key]; 473 encodeEntry(key, val); 474 if (i++ < dict.length - 1) 475 writer.put(' '); 476 } 477 } 478 if (useBraces == DictBraces.yes) 479 writer.put('}'); 480 } 481 unittest 482 { 483 auto dict = ["marker": marker, "num": 42.tag, "str": "a string".tag]; 484 assert(dict.zinc(SortedKeys.yes) == `{marker num:42 str:"a string"}`); 485 } 486 487 void encodeGridHeader(R)(auto ref const(Dict) meta, auto ref R writer, SortedKeys sorted = SortedKeys.no) 488 if (isOutputRange!(R, char)) 489 { 490 writer.put(`ver:"3.0"`); 491 if (meta.length > 0) 492 { 493 writer.put(' '); 494 meta.encode(writer, DictBraces.no, sorted); 495 } 496 writer.put('\n'); 497 } 498 499 /** 500 Encodes Grid as ver:"3.0" ... . 501 Expects an OutputRange as writer. 502 Returns: the writter OutputRange 503 */ 504 void encode(R) (auto ref const(Grid) grid, auto ref R writer, SortedKeys sorted = SortedKeys.no) 505 if (isOutputRange!(R, char)) 506 { 507 encodeGridHeader(grid.meta, writer, sorted); 508 if (grid.length == 0) 509 { 510 writer.put("empty"); 511 writer.put('\n'); 512 return; 513 } 514 515 string[] cols = cast(string[]) grid.colNames(); 516 if (sorted == SortedKeys.yes) 517 { 518 import std.array : array; 519 import std.algorithm : sort; 520 cols = cols.sort().array(); 521 } 522 523 foreach (size_t i, col; cols) 524 { 525 writer.put(col); 526 if (i < cols.length - 1) 527 writer.put(','); 528 } 529 writer.put('\n'); 530 foreach (immutable row; grid) 531 { 532 foreach (size_t colCnt, col; cols) 533 { 534 if (row.has(col) && row[col].hasValue) 535 row[col].encode(writer, sorted); 536 if (colCnt < cols.length - 1) 537 writer.put(','); 538 } 539 writer.put('\n'); 540 } 541 } 542 unittest 543 { 544 auto expect = "ver:\"3.0\"\n" 545 ~"empty\n"; 546 auto empty = Grid([]); 547 assert(empty.zinc(SortedKeys.yes) == expect); 548 // grid of scalars 549 auto grid = [ 550 ["marker": marker, "num": 42.tag, "str": "a string".tag], 551 ["marker": marker, "num": 100.tag, "str": "a string".tag] 552 ]; 553 expect = "ver:\"3.0\"\n" 554 ~"marker,num,str\n" 555 ~`M,42,"a string"` ~ "\n" 556 ~`M,100,"a string"` ~ "\n"; 557 string encoded = Grid(grid).zinc(SortedKeys.yes); 558 assert(encoded == expect); 559 // list and dict 560 grid = [ 561 ["list": [1.tag, true.tag].tag], 562 ["dict": ["a": 1.tag, "b": marker()].tag] 563 ]; 564 expect = "ver:\"3.0\"\n" 565 ~ "dict,list\n" 566 ~ ",[1,T]\n" 567 ~ "{a:1 b}," ~ "\n"; 568 encoded = Grid(grid).zinc(SortedKeys.yes); 569 assert(encoded == expect); 570 // grid of compound and scalar 571 grid = [ 572 ["type":"list".tag, "val": [1.tag, 2.tag, 3.tag].tag], 573 ["type":"dict".tag, "val": ["dis": "Dict!".tag, "foo": marker].tag], 574 ["type":"grid".tag, "val": Grid([["a": 1.tag, "b": 2.tag], ["a": 3.tag, "b": 4.tag]]).tag], 575 ["type":"scalar".tag, "val": "a scalar".tag], 576 ]; 577 expect = "ver:\"3.0\"\n" 578 ~"type,val\n" 579 ~`"list",[1,2,3]` ~ "\n" 580 ~`"dict",{dis:"Dict!" foo}` ~ "\n" 581 ~`"grid"`~",<<\n" 582 ~"ver:\"3.0\"\n" 583 ~"a,b\n" 584 ~"1,2\n" 585 ~"3,4\n" 586 ~`>>` ~ "\n" 587 ~`"scalar","a scalar"` ~ "\n"; 588 encoded = Grid(grid).zinc(SortedKeys.yes); 589 assert(encoded == expect); 590 // meta 591 scope metagrid = Grid([["a":1.tag]], ["foo": marker()]); 592 expect = `ver:"3.0" foo` ~"\n" 593 ~"a\n" 594 ~"1\n"; 595 encoded = zinc(metagrid, SortedKeys.yes); 596 assert(encoded == expect); 597 } 598 599 /** 600 Encodes any Tag type to Zinc using the $(D OutputRange) 601 */ 602 void zinc(T, R)(auto ref const(T) t, auto ref R writer, SortedKeys sorted = SortedKeys.no) 603 if (isOutputRange!(R, char)) 604 { 605 static if (is (T == TimeOfDay)) 606 { 607 Time(t).encode(writer); 608 } 609 else static if (is (T == DateTime)) 610 { 611 import std.datetime : UTC; 612 SysTime(t, UTC()).encode(writer); 613 } 614 else static if (is (T : const(Dict))) 615 { 616 t.encode(writer, DictBraces.yes, sorted); 617 } 618 else static if (is (T : const(Grid))) 619 { 620 t.encode(writer, sorted); 621 } 622 else 623 { 624 t.encode(writer); 625 } 626 } 627 628 /** 629 Encodes any Tag type to a Zinc string 630 */ 631 string zinc(T)(auto ref const(T) t, SortedKeys sorted = SortedKeys.no) 632 { 633 import std.array : appender; 634 auto buf = appender!string(); 635 zinc(t, buf, sorted); 636 return buf.data; 637 }