1 module exceeds_expectations.utils;
2 
3 import colorize;
4 import exceeds_expectations;
5 import exceeds_expectations.pretty_print;
6 import std.algorithm;
7 import std.conv;
8 import std.math;
9 import std.range;
10 import std.string;
11 import std.traits;
12 
13 
14 package string formatCode(string source, size_t focusLine, size_t radius)
15 in (
16     focusLine != 0,
17     "focusLine must be at least 1"
18 )
19 in (
20     focusLine < source.splitLines.length,
21     "focusLine must not be larger than the last line in the source (" ~
22     focusLine.to!string ~ " > " ~ source.splitLines.length.to!string ~ ")"
23 )
24 in (
25     source.splitLines.length > 0, "source may not be empty"
26 )
27 {
28     const string[] sourceLines = source[$ - 1] == '\n' ? source.splitLines ~ "" : source.splitLines;
29     immutable size_t sourceLength = sourceLines.length;
30     immutable size_t firstFocusLineIndex = focusLine - 1;
31     immutable size_t lastFocusLineIndex =
32         sourceLines[firstFocusLineIndex..$].countUntil!(e => e.canFind(';')) + firstFocusLineIndex;
33 
34     immutable size_t firstLineIndex =
35         firstFocusLineIndex.to!int - radius.to!int < 0 ?
36         0 :
37         firstFocusLineIndex - radius;
38 
39     immutable size_t lastLineIndex =
40         lastFocusLineIndex + radius >= sourceLength ?
41         sourceLength - 1 :
42         lastFocusLineIndex + radius;
43 
44     return
45         sourceLines[firstLineIndex .. lastLineIndex + 1]
46         .map!convertTabsToSpaces
47         .zip(iota(firstLineIndex, lastLineIndex + 1))
48         .map!((tup) {
49             immutable string lineContents = (tup[1] + 1).to!string.padLeft(' ', 4).to!string ~ " | " ~ tup[0];
50             return
51                 tup[1] >= firstFocusLineIndex && tup[1] <= lastFocusLineIndex ?
52                 lineContents.color(fg.yellow) :
53                 truncate(lineContents, 120);
54         })
55         .join('\n') ~ "\n";
56 }
57 
58 private string convertTabsToSpaces(string line)
59 {
60     if (line.length == 0 || line[0] != '\t')
61     {
62         return line;
63     }
64     else
65     {
66         return "    " ~ convertTabsToSpaces(line[1..$]);
67     }
68 }
69 
70 @("convertTabsToSpaces — empty line")
71 unittest
72 {
73     expect(convertTabsToSpaces("")).toEqual("");
74 }
75 
76 @("convertTabsToSpaces — no indentation")
77 unittest
78 {
79     expect(convertTabsToSpaces("Test\tHello World\t\t  ")).toEqual("Test\tHello World\t\t  ");
80 }
81 
82 @("convertTabsToSpaces — tabs indentation")
83 unittest
84 {
85     expect(convertTabsToSpaces("\t\t\tTest\tHello World\t\t  ")).toEqual("            Test\tHello World\t\t  ");
86 }
87 
88 private string truncate(string line, int length)
89 in(length >= 0, "Cannot truncate line to length " ~ length.to!string)
90 {
91     if (line.length > length)
92     {
93         return line[0 .. length - 4] ~ " ...".color(fg.light_black);
94     }
95 
96     return line;
97 }
98 
99 @("Truncate empty line")
100 unittest
101 {
102     expect(truncate("", 80)).toEqual("");
103 }
104 
105 @("Truncate non-empty line to a given length")
106 unittest
107 {
108     expect(truncate("abcdefghijklmnopqrstuvwxyz", 10)).toEqual("abcdef" ~ " ...".color(fg.light_black));
109 }
110 
111 @("Truncate — edge cases")
112 unittest
113 {
114     expect(truncate("abcdefghij", 10)).toEqual("abcdefghij");
115     expect(truncate("abcdefghijk", 10)).toEqual("abcdef" ~ " ...".color(fg.light_black));
116 }
117 
118 
119 /// Returns a string showing the expected and received values. Ends with a line separator.
120 package string formatDifferences(string expected, string received, bool not)
121 {
122     immutable string lineLabel1 = (not ? "Forbidden: " : "Expected: ").color(fg.green);
123     immutable string lineLabel2 = (not ? "Received:  " : "Received: ").color(fg.light_red);
124     string expectedString = lineLabel1 ~ expected ~ (expected.canFind('\n') ? "\n" : "");
125     string receivedString = lineLabel2 ~ received;
126     return expectedString ~ "\n" ~ receivedString ~ "\n";
127 }
128 
129 
130 package string formatApproxDifferences(TReceived, TExpected, F : real)(
131     const auto ref TExpected expected,
132     const auto ref TReceived received,
133     F maxRelDiff = CommonDefaultFor!(TReceived, TExpected),
134     F maxAbsDiff = 0.0
135 )
136 {
137     static string getOrderOperator(real lhs, real rhs)
138     {
139         if (lhs > rhs)
140             return " > ";
141         else if (lhs < rhs)
142             return " < ";
143         else
144             return " = ";
145     }
146 
147     immutable real relDiff = fabs((received - expected) / expected);
148     immutable real absDiff = fabs(received - expected);
149 
150     return
151         "Relative Difference: ".color(fg.yellow) ~
152         prettyPrint(relDiff) ~ getOrderOperator(relDiff, maxRelDiff) ~ prettyPrint(maxRelDiff) ~
153         " (maxRelDiff)\n" ~
154 
155         "Absolute Difference: ".color(fg.yellow) ~
156         prettyPrint(absDiff) ~ getOrderOperator(absDiff, maxAbsDiff) ~ prettyPrint(maxAbsDiff) ~
157         " (maxAbsDiff)\n";
158 }
159 
160 
161 package string formatTypeDifferences(TypeInfo expected, TypeInfo received, bool not)
162 {
163     static string indentAllExceptFirst(string text, int numSpaces)
164     {
165         return text
166             .splitLines()
167             .enumerate()
168             .map!(
169                 idxValuePair => (
170                     idxValuePair[0] == 0 ?
171                     idxValuePair[1] :
172                     ' '.repeat(numSpaces).array.to!string ~ idxValuePair[1]
173                 )
174             )
175             .join("\n");
176     }
177 
178     return formatDifferences(
179         prettyPrint(expected),
180         indentAllExceptFirst(
181             prettyPrintInheritanceTree(received),
182             not ? 11 : 10
183         ),
184         not
185     );
186 }
187 
188 
189 /// Given an array of orderable elements,
190 ///
191 /// Example:
192 ///     Given `[]` return ""
193 ///
194 /// Example:
195 ///     Given `[1]` return "1"
196 ///
197 /// Example:
198 ///     Given `[3, 0]` return "0 and 3"
199 ///
200 /// Example:
201 ///     Given `[1, 0, 3]` returns "0, 1, and 3"
202 package string humanReadableNumbers(N)(N[] numbers)
203 if (isOrderingComparable!N)
204 {
205     if (numbers.length == 0) return "";
206 
207     auto strings = numbers.sort.map!(e => e.to!string);
208 
209     return (
210         strings.length == 1 ? strings[0] :
211         strings.length == 2 ? strings.join(" and ") :
212         strings[0 .. $ - 1].join(", ") ~ ", and " ~ strings[$ - 1]
213     );
214 }
215 
216 
217 /// This is defined in std.math but it's private.
218 package template CommonDefaultFor(T,U)
219 {
220     import std.algorithm.comparison : min;
221 
222     alias baseT = FloatingPointBaseType!T;
223     alias baseU = FloatingPointBaseType!U;
224 
225     enum CommonType!(baseT, baseU) CommonDefaultFor = 10.0L ^^ -((min(baseT.dig, baseU.dig) + 1) / 2 + 1);
226 }
227 
228 /// ditto
229 private template FloatingPointBaseType(T)
230 {
231     import std.range.primitives : ElementType;
232     static if (isFloatingPoint!T)
233     {
234         alias FloatingPointBaseType = Unqual!T;
235     }
236     else static if (isFloatingPoint!(ElementType!(Unqual!T)))
237     {
238         alias FloatingPointBaseType = Unqual!(ElementType!(Unqual!T));
239     }
240     else
241     {
242         alias FloatingPointBaseType = real;
243     }
244 }
245 
246 package enum bool canCompareForEquality(L, R) = __traits(compiles, rvalueOf!L == rvalueOf!R);