1 /** 2 Some additional range functions. 3 4 Copyright: © 2019 Arne Ludwig <arne.ludwig@posteo.de> 5 License: Subject to the terms of the MIT license, as written in the 6 included LICENSE file. 7 Authors: Arne Ludwig <arne.ludwig@posteo.de> 8 */ 9 module dalicious.range; 10 11 import std.algorithm : map; 12 import std.functional : unaryFun; 13 import std.meta : AliasSeq, staticMap; 14 import std.range : 15 chain, 16 ElementEncodingType, 17 ElementType, 18 hasLength, 19 hasSlicing, 20 iota, 21 isInputRange; 22 import std.range.primitives; 23 import std.traits : isSomeChar, rvalueOf; 24 import std.typecons : tuple, Tuple; 25 26 /** 27 This range iterates over fixed-sized chunks of size chunkSize of a source 28 range. Source must be an input range. chunkSize must be greater than zero. 29 30 See Also: `std.range.chunks` 31 Returns: Range of chunks, ie. `ElementType!Source[]`. 32 */ 33 auto arrayChunks(Source)(Source range, in size_t chunkSize) if (isInputRange!Source) 34 { 35 alias Element = ElementType!Source; 36 37 static struct ArrayChunks 38 { 39 private Source range; 40 private const size_t chunkSize; 41 private Element[] chunk; 42 43 this(Source range, in size_t chunkSize) 44 { 45 this.range = range; 46 this.chunkSize = chunkSize; 47 this.popFront(); 48 } 49 50 void popFront() 51 { 52 if (range.empty) 53 { 54 chunk = null; 55 56 return; 57 } 58 59 chunk = new Element[chunkSize]; 60 61 foreach (i; 0 .. chunkSize) 62 { 63 chunk[i] = range.front; 64 65 if (range.empty) 66 { 67 chunk = chunk[0 .. i + 1]; 68 break; 69 } 70 else 71 { 72 range.popFront(); 73 } 74 } 75 } 76 77 @property Element[] front() 78 { 79 return chunk; 80 } 81 82 @property bool empty() 83 { 84 return chunk is null; 85 } 86 } 87 88 assert(chunkSize > 0, "chunkSize must be greater than zero"); 89 90 return ArrayChunks(range, chunkSize); 91 } 92 93 /// 94 unittest 95 { 96 import std.array : array; 97 import std.range : iota; 98 99 auto chunks = iota(10).arrayChunks(2); 100 assert(chunks.array == [[0, 1], [2, 3], [4, 5], [6, 7], [8, 9]]); 101 } 102 103 /** 104 Generate a range of `num` even-sized slices. 105 106 Always returns a range of slices in contrast to `std.range.evenChunks` 107 which returns a range of `take`s. 108 109 See Also: `std.range.evenChunks` 110 Returns: Range of even slices. 111 */ 112 auto evenSlices(Source)(Source range, in size_t sliceCount) 113 if (isInputRange!Source && hasLength!Source && hasSlicing!Source) 114 { 115 assert(sliceCount > 0, "sliceCount must be positive"); 116 117 auto sliceSize = range.length / sliceCount; 118 auto numLargerSlices = range.length % sliceCount; 119 120 return chain( 121 iota(numLargerSlices).map!(i => range[i*(sliceSize + 1) .. (i + 1)*(sliceSize + 1)]), 122 iota(numLargerSlices, sliceCount).map!(i => range[i*sliceSize + numLargerSlices .. (i + 1)*sliceSize + numLargerSlices]), 123 ); 124 } 125 126 /// 127 unittest 128 { 129 import std.algorithm : equal; 130 import std.range : iota; 131 132 auto slices = iota(10).evenSlices(3); 133 assert(equal!equal(slices, [ 134 [0, 1, 2, 3], 135 [4, 5, 6], 136 [7, 8, 9], 137 ])); 138 } 139 140 /// Generate a tuple of tuples of chunkSize. 141 template chunks(size_t chunkSize) 142 { 143 auto chunks(T...)(T args) pure nothrow @safe if (args.length >= chunkSize) 144 { 145 return tuple(tuple(args[0 .. chunkSize]), chunks(args[chunkSize .. $]).expand); 146 } 147 148 auto chunks(T...)(T args) pure nothrow @safe 149 if (0 < args.length && args.length < chunkSize) 150 { 151 return tuple(tuple(args[0 .. $])); 152 } 153 154 auto chunks(T...)(T args) pure nothrow @safe if (args.length == 0) 155 { 156 return tuple(); 157 } 158 } 159 160 /// 161 unittest 162 { 163 auto c1 = chunks!2(0, 1, 2, 3, 4, 5); 164 165 assert(c1 == tuple(tuple(0, 1), tuple(2, 3), tuple(4, 5))); 166 167 auto c2 = chunks!3(false, "1", 2.0, 3, '4', 5); 168 169 assert(c2 == tuple(tuple(false, "1", 2.0), tuple(3, '4', 5))); 170 171 enum c4 = chunks!4(false, "1", 2.0, 3, '4', 5); 172 173 static assert(c4 == tuple(tuple(false, "1", 2.0, 3), tuple('4', 5))); 174 } 175 176 /// Split a list of aliases into chunks. 177 template Chunks(size_t chunkSize, T...) 178 { 179 static if (T.length >= chunkSize) 180 { 181 alias Chunks = AliasSeq!(Chunk!(T[0 .. chunkSize]), Chunks!(chunkSize, T[chunkSize .. $])); 182 } 183 else static if (0 < T.length && T.length < chunkSize) 184 { 185 alias Chunks = AliasSeq!(Chunk!(T[0 .. $])); 186 } 187 else static if (T.length == 0) 188 { 189 alias Chunks = AliasSeq!(); 190 } 191 else 192 { 193 static assert(0); 194 } 195 } 196 197 template Chunk(T...) 198 { 199 struct Chunk 200 { 201 alias chunks = T; 202 } 203 } 204 205 /// 206 unittest 207 { 208 alias c1 = Chunks!(2, AliasSeq!(int, int, int, int, int, int)); 209 210 static assert(is(c1 == AliasSeq!(Chunk!(int, int), Chunk!(int, int), Chunk!(int, int)))); 211 static foreach (pair; c1) 212 { 213 static foreach (type; pair.chunks) 214 { 215 static assert(is(type == int)); 216 } 217 } 218 } 219 220 /* 221 Build a comparator according to `pred`. 222 */ 223 template Comparator(pred...) if (pred.length == 1) 224 { 225 /// Return comparison value akin to `opCmp`. 226 int compare(S, T = S)(in S a, in T b) 227 { 228 alias _pred = unaryFun!pred; 229 auto _a = _pred(a); 230 auto _b = _pred(b); 231 232 if (_a < _b) 233 return -1; 234 else if (_a == _b) 235 return 0; 236 else 237 return 1; 238 } 239 240 /// Return `true` iff `a < b`. 241 bool lt(S, T = S)(in S a, in T b) 242 { 243 return compare!(S, T)(a, b) < 0; 244 } 245 246 /// Return `true` iff `a <= b`. 247 bool le(S, T = S)(in S a, in T b) 248 { 249 return compare!(S, T)(a, b) <= 0; 250 } 251 252 /// Return `true` iff `a == b`. 253 bool eq(S, T = S)(in S a, in T b) 254 { 255 return compare!(S, T)(a, b) == 0; 256 } 257 258 /// Return `true` iff `a >= b`. 259 bool ge(S, T = S)(in S a, in T b) 260 { 261 return compare!(S, T)(a, b) >= 0; 262 } 263 264 /// Return `true` iff `a > b`. 265 bool gt(S, T = S)(in S a, in T b) 266 { 267 return compare!(S, T)(a, b) > 0; 268 } 269 } 270 271 /// 272 unittest 273 { 274 alias compareSquares = Comparator!"a ^^ 2".compare; 275 276 assert(compareSquares(1, 2) < 0); 277 assert(compareSquares(1, -2) < 0); 278 assert(compareSquares(-1, 1) == 0); 279 assert(compareSquares(-2.0, 1) > 0); 280 281 alias compareByLength = Comparator!"a.length".compare; 282 283 assert(compareByLength([], [1]) < 0); 284 assert(compareByLength([1, 2], [1]) > 0); 285 assert(compareByLength([1, 2], ["1", "2"]) == 0); 286 287 alias compareAbsInts = Comparator!("a > 0 ? a : -a").compare!(int); 288 289 assert(compareSquares(1, 2) < 0); 290 assert(compareSquares(1, -2) < 0); 291 assert(compareSquares(-1, 1) == 0); 292 assert(compareSquares(-2, 1) > 0); 293 static assert(!__traits(compiles, compareAbsInts(-2.0, 1.0))); 294 295 alias ltSquared = Comparator!("a ^^ 2").lt; 296 297 assert(ltSquared(1, 2)); 298 assert(ltSquared(1, -2)); 299 assert(!ltSquared(-2, -1)); 300 301 alias eqSquared = Comparator!("a ^^ 2").eq; 302 303 assert(eqSquared(1, 1)); 304 assert(eqSquared(1, -1)); 305 assert(!eqSquared(1, 2)); 306 } 307 308 /// Take exactly `n` element from range. Throws an exception if range has not 309 /// enough elements. 310 /// 311 /// Throws: Exception if range has less than `n` elements. 312 ElementType!R[n] takeExactly(size_t n, R)(R range) if (isInputRange!R) 313 { 314 import std.exception : enforce; 315 316 ElementType!R[n] result; 317 size_t i = 0; 318 319 while (!range.empty && i < n) 320 { 321 result[i++] = range.front; 322 range.popFront(); 323 } 324 325 enforce!Exception(i == n, "not enough elements"); 326 327 return result; 328 } 329 330 /// 331 unittest 332 { 333 import std.exception : assertThrown; 334 import std.range : iota; 335 336 static assert(is(typeof(iota(10).takeExactly!5) == int[5])); 337 assert(iota(10).takeExactly!5 == [0, 1, 2, 3, 4]); 338 339 assertThrown!Exception(iota(2).takeExactly!5); 340 } 341 342 class WrapLinesImpl(R) 343 { 344 R output; 345 size_t lineWidth; 346 size_t column; 347 348 this(R output, size_t lineWidth) 349 { 350 this.output = output; 351 this.lineWidth = lineWidth; 352 } 353 354 void put(inout(char) c) 355 { 356 import std.range.primitives; 357 358 assert(c == '\n' || column < lineWidth); 359 360 std.range.primitives.put(output, c); 361 ++column; 362 363 if (c == '\n') 364 { 365 column = 0; 366 } 367 368 if (column >= lineWidth) 369 { 370 put('\n'); 371 } 372 } 373 374 void put(string chunk) 375 { 376 foreach (c; chunk) 377 { 378 put(c); 379 } 380 } 381 } 382 383 auto wrapLines(R)(R output, size_t lineWidth) 384 { 385 return new WrapLinesImpl!R(output, lineWidth); 386 } 387 388 unittest 389 { 390 import std.range.primitives : put; 391 392 auto outputBuffer = new dchar[12]; 393 auto output = wrapLines(outputBuffer, 10); 394 395 put(output, "hello world"); 396 397 assert(outputBuffer == "hello worl\nd"); 398 } 399 400 /// Return a tuple of `fun` applied to each value of `tuple`. 401 auto tupleMap(alias fun, Types...)(Types values) 402 { 403 import std.format : format; 404 405 alias mapper = unaryFun!fun; 406 407 enum makeTuple = format!`tuple(%-(mapper(values[%d])%|, %))`(iota(values.length)); 408 409 return mixin(makeTuple); 410 } 411 412 /// 413 unittest 414 { 415 import std.conv : to; 416 import std.typecons : tuple; 417 418 assert( 419 tupleMap!"2*a"(1, 2, 3.0) == 420 tuple(2, 4, 6.0) 421 ); 422 assert( 423 tupleMap!(x => to!string(x))(1, '2', 3.0) == 424 tuple("1", "2", "3") 425 ); 426 } 427 428 429 /// Wraps given input range and keeps track of the line number and column. 430 auto trackLineLocation(R)(R input, size_t line = 1, size_t column = 1) pure nothrow @safe @nogc 431 if (isInputRange!R && isSomeChar!(ElementEncodingType!R)) 432 { 433 static struct LineLocationTracker 434 { 435 R input; 436 size_t line; 437 size_t column; 438 439 440 void popFront() 441 { 442 assert(!empty, "Attempting to popFront an empty " ~ typeof(this).stringof); 443 444 if (front == '\n') 445 { 446 ++line; 447 column = 0; 448 } 449 450 ++column; 451 input.popFront(); 452 } 453 454 455 @property auto front() 456 { 457 assert(!empty, "Attempting to fetch the front of an empty " ~ typeof(this).stringof); 458 459 return input.front; 460 } 461 462 463 @property bool empty() 464 { 465 return input.empty; 466 } 467 468 469 static if (hasLength!R) 470 @property auto length() 471 { 472 return input.length; 473 } 474 } 475 476 return LineLocationTracker(input, line, column); 477 } 478 479 /// 480 unittest 481 { 482 import std.array; 483 import std.range; 484 485 enum testDocLines = [ 486 "line 1", 487 "line 2", 488 "line 3", 489 "line 4", 490 ]; 491 auto testDoc = testDocLines.join('\n') ~ '\n'; 492 493 auto tracker = trackLineLocation(refRange(&testDoc)); 494 // default tracker starts at line 1 column 1 495 assert(tracker.line == 1); 496 assert(tracker.column == 1); 497 498 // read to end of first line 499 tracker.popFrontExactly(testDocLines[0].length); 500 // still on first line but column changed to last column in the line 501 // which is the newline itself 502 assert(tracker.line == 1); 503 assert(tracker.column == testDocLines[0].length + 1); 504 505 // pop the newline 506 tracker.popFront(); 507 // now we are on line 2 column 1 508 assert(tracker.line == 2); 509 assert(tracker.column == 1); 510 511 // read until the end 512 tracker.popFrontN(testDoc.length); 513 assert(tracker.line == 5); 514 assert(tracker.column == 1); 515 }