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);