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 }