1 module toml_foolery.encode.types.floating_point;
2 
3 import std.math : modf, isNaN;
4 import std.traits : isFloatingPoint;
5 
6 import toml_foolery.encode;
7 
8 
9 package(toml_foolery.encode) enum bool makesTomlFloat(T) = (
10     isFloatingPoint!T
11 );
12 
13 /// Serializes float, double, and real into TOML floating point values.
14 /// TOML floats are always 64-bit, floats and reals are converted to doubles
15 /// first.
16 package(toml_foolery.encode) void tomlifyValueImpl(T)(
17     const T value,
18     ref Appender!string buffer,
19     immutable string[] parentTables
20 )
21 if (makesTomlFloat!T)
22 {
23     if (value == T(0.0))
24     {
25         buffer.put("0.0");
26     }
27     else if (value == T.infinity)
28     {
29         buffer.put("inf");
30     }
31     else if (value == -T.infinity)
32     {
33         buffer.put("-inf");
34     }
35     else if (value.isNaN)
36     {
37         buffer.put("nan");
38     }
39     else
40     {
41         real integralPartF;
42         real fractionalPart = modf(value.to!real, integralPartF);
43         size_t fracPartLength = fractionalPart.to!string.length;
44         size_t decimals =
45             fracPartLength >= 3 ?
46             fracPartLength - 2 :
47             1;
48 
49         buffer.put("%,?.*f".format(decimals, '_', value));
50     }
51 }
52 
53 version(unittest)
54 {
55     import std.meta : AliasSeq;
56     static foreach (type; AliasSeq!(float, double, real))
57     {
58         mixin(fpTest!type);
59     }
60 }
61 
62 // I tried using a mixin template for this, it compiled but the tests didn't
63 // show up in the output.
64 private string fpTest(T)()
65 if (isFloatingPoint!T)
66 {
67     return
68     `
69     @("Encode ` ~ "`" ~ T.stringof ~ "`" ~ ` values — non-weird")
70     unittest
71     {
72         ` ~ T.stringof ~ ` zero = ` ~ T.stringof ~ `(0.0f);
73         expect(_tomlifyValue(zero)).toEqual("0.0");
74 
75         ` ~ T.stringof ~ ` one = ` ~ T.stringof ~ `(1.0f);
76         expect(_tomlifyValue(one)).toEqual("1.0");
77 
78         ` ~ T.stringof ~ ` negOne = ` ~ T.stringof ~ `(-1.0f);
79         expect(_tomlifyValue(negOne)).toEqual("-1.0");
80 
81         ` ~ T.stringof ~ ` po2 = ` ~ T.stringof ~ `(512.5);
82         expect(_tomlifyValue(po2)).toEqual("512.5");
83     }
84 
85     @("Encode ` ~ "`" ~ T.stringof ~ "`" ~ ` values — weird")
86     unittest
87     {
88         // Negative zero and negative NaN are not supported.
89         // They just become 0 and NaN.
90 
91         ` ~ T.stringof ~ ` negZero = ` ~ T.stringof ~ `(-0.0f);
92         expect(_tomlifyValue(negZero)).toEqual("0.0");
93 
94         ` ~ T.stringof ~ ` posInf = ` ~ T.stringof ~ `(` ~ T.stringof ~ `.infinity);
95         expect(_tomlifyValue(posInf)).toEqual("inf");
96 
97         ` ~ T.stringof ~ ` negInf = ` ~ T.stringof ~ `(-` ~ T.stringof ~ `.infinity);
98         expect(_tomlifyValue(negInf)).toEqual("-inf");
99 
100         ` ~ T.stringof ~ ` posNan = ` ~ T.stringof ~ `(` ~ T.stringof ~ `.nan);
101         expect(_tomlifyValue(posNan)).toEqual("nan");
102 
103         ` ~ T.stringof ~ ` negNan = ` ~ T.stringof ~ `(-` ~ T.stringof ~ `.nan);
104         expect(_tomlifyValue(negNan)).toEqual("nan");
105     }
106     `
107     ;
108 }