{"id":47457355,"url":"https://github.com/OrlovEvgeny/lo.zig","last_synced_at":"2026-04-07T08:01:18.457Z","repository":{"id":344794685,"uuid":"1182274010","full_name":"OrlovEvgeny/lo.zig","owner":"OrlovEvgeny","description":"A Lodash-style Zig library","archived":false,"fork":false,"pushed_at":"2026-03-16T15:21:40.000Z","size":3865,"stargazers_count":16,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2026-03-16T22:59:29.344Z","etag":null,"topics":["lodash","zig-package"],"latest_commit_sha":null,"homepage":"","language":"Zig","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/OrlovEvgeny.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-15T09:39:20.000Z","updated_at":"2026-03-16T20:50:24.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/OrlovEvgeny/lo.zig","commit_stats":null,"previous_names":["orlovevgeny/lo.zig"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/OrlovEvgeny/lo.zig","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/OrlovEvgeny%2Flo.zig","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/OrlovEvgeny%2Flo.zig/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/OrlovEvgeny%2Flo.zig/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/OrlovEvgeny%2Flo.zig/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/OrlovEvgeny","download_url":"https://codeload.github.com/OrlovEvgeny/lo.zig/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/OrlovEvgeny%2Flo.zig/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31504897,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-07T03:10:19.677Z","status":"ssl_error","status_checked_at":"2026-04-07T03:10:13.982Z","response_time":105,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["lodash","zig-package"],"created_at":"2026-03-24T00:00:35.806Z","updated_at":"2026-04-07T08:01:18.442Z","avatar_url":"https://github.com/OrlovEvgeny.png","language":"Zig","readme":"[![CI](https://github.com/OrlovEvgeny/lo.zig/actions/workflows/ci.yml/badge.svg)](https://github.com/OrlovEvgeny/lo.zig/actions/workflows/ci.yml)\n[![Release](https://img.shields.io/github/v/release/OrlovEvgeny/lo.zig)](https://github.com/OrlovEvgeny/lo.zig/releases/latest)\n[![Zig](https://img.shields.io/badge/zig-0.15.0-F7A41D?logo=zig\u0026logoColor=white)](https://ziglang.org)\n\n# lo.zig is a Lodash-style Zig library\n\n\u003cp align=\"center\"\u003e\n\u003cimg src=\"assets/logo.png\" alt=\"lo.zig\" width=\"900\"/\u003e\n\u003c/p\u003e\n\nGeneric utility library for Zig\n\nZero hidden allocations: functions that need memory take an `Allocator`.\nIterator-first: most transformations return lazy iterators.\n\n## Installation\n\nAdd `lo.zig` as a dependency in your `build.zig.zon`:\n\n```sh\nzig fetch --save git+https://github.com/OrlovEvgeny/lo.zig\n```\n\nThen in your `build.zig`:\n\n```zig\nconst lo_dep = b.dependency(\"lo\", .{\n    .target = target,\n    .optimize = optimize,\n});\nexe.root_module.addImport(\"lo\", lo_dep.module(\"lo\"));\n```\n\n## Quick Start\n\n```zig\nconst lo = @import(\"lo\");\n\nconst total = lo.sum(i32, \u0026.{ 1, 2, 3, 4 }); // 10\nconst head  = lo.first(i32, \u0026.{ 10, 20, 30 }); // 10\nconst safe  = lo.unwrapOr(i32, null, 42); // 42\n```\n\n## Function Index\n\n- [Slice Helpers](#slice-helpers) - first, last, nth, firstOr, lastOr, nthOr, initial, tail, drop, dropRight, dropWhile, dropWhileAlloc, dropRightWhile, take, takeRight, takeWhile, takeWhileAlloc, takeRightWhile, sample, samples\n- [Transform](#transform) - map, mapAlloc, mapIndex, filter, filterAlloc, reject, rejectAlloc, compact, compactAlloc, flatten, flattenAlloc, flattenDeep, flatMap, flatMapAlloc, without, forEach, forEachIndex, compactMap, filterMapIter\n- [Aggregate](#aggregate) - reduce, reduceRight, sum, sumBy, product, productBy, mean, meanBy, min, max, minBy, maxBy, minMax, minMaxBy, count, countBy, countValues, mode, median, variance, stddev, sampleVariance, sampleStddev, percentile\n- [Sort \u0026 Order](#sort--order) - sortBy, sortByAlloc, sortByField, sortByFieldAlloc, toSortedAlloc, isSorted, equal, reverse, shuffle\n- [Set Operations](#set-operations) - uniq, uniqBy, intersect, union\\_, difference, symmetricDifference, findDuplicates, findUniques, elementsMatch, differenceWith, intersectWith, unionWith\n- [Partition \u0026 Group](#partition--group) - partition, groupBy, chunk, window, scan, scanAlloc\n- [Combine](#combine) - concat, splice, interleave, fill, fillRange, repeat, repeatBy, times, timesAlloc\n- [Search](#search) - find, findIndex, findLast, findLastIndex, indexOf, lastIndexOf, contains, containsBy, every, some, none, minIndex, maxIndex, binarySearch, lowerBound, upperBound, sortedIndex, sortedLastIndex\n- [Map Helpers](#map-helpers) - keys, keysAlloc, values, valuesAlloc, entries, entriesAlloc, fromEntries, mapKeys, mapValues, filterMap, filterKeys, filterValues, pickKeys, omitKeys, invert, merge, assign, mapEntries, mapToSlice, valueOr, hasKey, mapCount, keyBy, associate\n- [String Helpers](#string-helpers) - words, wordsAlloc, camelCase, pascalCase, snakeCase, kebabCase, capitalize, lowerFirst, toLower, toUpper, trim, trimStart, trimEnd, startsWith, endsWith, includes, substr, ellipsis, strRepeat, padLeft, padRight, runeLength, randomString, split, splitAlloc, join, replace, replaceAll, chunkString\n- [Math](#math) - sum, mean, median, variance, stddev, sampleVariance, sampleStddev, percentile, lerp, remap, clamp, inRange, cumSum, cumProd, rangeAlloc, rangeWithStepAlloc\n- [Tuple Helpers](#tuple-helpers) - zip, zipAlloc, zipWith, unzip, enumerate\n- [Type Helpers](#type-helpers) - isNull, isNotNull, unwrapOr, coalesce, empty, isEmpty, isNotEmpty, ternary, toConst\n- [Types](#types) - Entry, Pair, MinMax, RangeError, PartitionResult, UnzipResult, AssocEntry, and iterator types\n\n---\n\n## Slice Helpers\n\n### first\n\nReturns the first element of a slice, or null if empty.\n\n```zig\nlo.first(i32, \u0026.{ 10, 20, 30 }); // 10\n```\n\n### last\n\nReturns the last element of a slice, or null if empty.\n\n```zig\nlo.last(i32, \u0026.{ 10, 20, 30 }); // 30\n```\n\n### nth\n\nElement at the given index. Negative indices count from the end. Returns null if out of bounds.\n\n```zig\nlo.nth(i32, \u0026.{ 10, 20, 30 }, -1); // 30\n```\n\n### firstOr\n\nReturns the first element, or a default if the slice is empty.\n\n```zig\nlo.firstOr(i32, \u0026.{ 10, 20, 30 }, 0); // 10\nlo.firstOr(i32, \u0026.{}, 42);             // 42\n```\n\n### lastOr\n\nReturns the last element, or a default if the slice is empty.\n\n```zig\nlo.lastOr(i32, \u0026.{ 10, 20, 30 }, 0); // 30\nlo.lastOr(i32, \u0026.{}, 42);             // 42\n```\n\n### nthOr\n\nElement at the given index with a default. Negative indices count from the end.\n\n```zig\nlo.nthOr(i32, \u0026.{ 10, 20, 30 }, 1, 0);  // 20\nlo.nthOr(i32, \u0026.{ 10, 20, 30 }, -1, 0); // 30\nlo.nthOr(i32, \u0026.{ 10, 20, 30 }, 5, 99); // 99\n```\n\n### initial\n\nAll elements except the last. Empty slice if input is empty.\n\n```zig\nlo.initial(i32, \u0026.{ 1, 2, 3 }); // \u0026.{ 1, 2 }\n```\n\n### tail\n\nAll elements except the first. Empty slice if input is empty.\n\n```zig\nlo.tail(i32, \u0026.{ 1, 2, 3 }); // \u0026.{ 2, 3 }\n```\n\n### drop\n\nRemove the first n elements, returning the rest as a sub-slice.\n\n```zig\nlo.drop(i32, \u0026.{ 1, 2, 3, 4, 5 }, 2); // \u0026.{ 3, 4, 5 }\n```\n\n### dropRight\n\nRemove the last n elements, returning the rest as a sub-slice.\n\n```zig\nlo.dropRight(i32, \u0026.{ 1, 2, 3, 4, 5 }, 2); // \u0026.{ 1, 2, 3 }\n```\n\n### dropWhile\n\nDrop leading elements while the predicate returns true.\n\n```zig\nlo.dropWhile(i32, \u0026.{ 1, 2, 3, 4 }, isLessThan3); // \u0026.{ 3, 4 }\n```\n\n### dropWhileAlloc\n\nDrop leading elements while the predicate returns true. Allocates a copy. Caller owns the returned slice.\n\n```zig\nconst result = try lo.dropWhileAlloc(i32, allocator, \u0026.{ 1, 2, 3, 4 }, isLessThan3);\ndefer allocator.free(result);\n// result == \u0026.{ 3, 4 }\n```\n\n### dropRightWhile\n\nDrop trailing elements while the predicate returns true.\n\n```zig\nlo.dropRightWhile(i32, \u0026.{ 1, 2, 3, 4 }, isGt2); // \u0026.{ 1, 2 }\n```\n\n### take\n\nTake the first n elements as a sub-slice.\n\n```zig\nlo.take(i32, \u0026.{ 1, 2, 3, 4, 5 }, 3); // \u0026.{ 1, 2, 3 }\n```\n\n### takeRight\n\nTake the last n elements as a sub-slice.\n\n```zig\nlo.takeRight(i32, \u0026.{ 1, 2, 3, 4, 5 }, 2); // \u0026.{ 4, 5 }\n```\n\n### takeWhile\n\nTake leading elements while the predicate returns true.\n\n```zig\nlo.takeWhile(i32, \u0026.{ 1, 2, 3, 4 }, isLessThan3); // \u0026.{ 1, 2 }\n```\n\n### takeWhileAlloc\n\nTake leading elements while the predicate returns true. Allocates a copy. Caller owns the returned slice.\n\n```zig\nconst result = try lo.takeWhileAlloc(i32, allocator, \u0026.{ 1, 2, 3, 4 }, isLessThan3);\ndefer allocator.free(result);\n// result == \u0026.{ 1, 2 }\n```\n\n### takeRightWhile\n\nTake trailing elements while the predicate returns true.\n\n```zig\nlo.takeRightWhile(i32, \u0026.{ 1, 2, 3, 4 }, isGt2); // \u0026.{ 3, 4 }\n```\n\n### sample\n\nRandom element from a slice. Null if empty.\n\n```zig\nvar prng = std.Random.DefaultPrng.init(0);\nlo.sample(i32, \u0026.{ 1, 2, 3 }, prng.random()); // random element\n```\n\n### samples\n\nN random elements from a slice (with replacement). Caller owns the returned slice.\n\n```zig\nconst s = try lo.samples(i32, allocator, \u0026.{ 1, 2, 3 }, 5, rng);\ndefer allocator.free(s);\n```\n\n---\n\n## Transform\n\n### map\n\nTransform each element. Returns a lazy iterator.\n\n```zig\nvar it = lo.map(i32, i64, \u0026.{ 1, 2, 3 }, double);\nit.next(); // 2\n```\n\n### mapAlloc\n\nTransform each element and collect into an allocated slice. Caller owns the returned slice.\n\n```zig\nconst result = try lo.mapAlloc(i32, i32, allocator, \u0026.{ 1, 2, 3 }, double);\ndefer allocator.free(result);\n```\n\n### mapIndex\n\nTransform each element with its index. Returns a lazy iterator.\n\n```zig\nvar it = lo.mapIndex(i32, i64, \u0026.{ 10, 20 }, addIndex);\nit.next(); // addIndex(10, 0)\n```\n\n### filter\n\nKeep elements matching the predicate. Returns a lazy iterator.\n\n```zig\nvar it = lo.filter(i32, \u0026.{ 1, 2, 3, 4 }, isEven);\nit.next(); // 2\nit.next(); // 4\n```\n\n### filterAlloc\n\nKeep elements matching the predicate, collected into an allocated slice. Caller owns the returned slice.\n\n```zig\nconst result = try lo.filterAlloc(i32, allocator, \u0026.{ 1, 2, 3, 4 }, isEven);\ndefer allocator.free(result);\n```\n\n### reject\n\nRemove elements matching the predicate. Returns a lazy iterator.\n\n```zig\nvar it = lo.reject(i32, \u0026.{ 1, 2, 3, 4 }, isEven);\nit.next(); // 1\nit.next(); // 3\n```\n\n### rejectAlloc\n\nRemove elements matching the predicate, collected into an allocated slice. Caller owns the returned slice.\n\n```zig\nconst result = try lo.rejectAlloc(i32, allocator, \u0026.{ 1, 2, 3, 4 }, isEven);\ndefer allocator.free(result);\n```\n\n### compact\n\nRemove zero/null/default values. Returns a lazy iterator.\n\n```zig\nvar it = lo.compact(?i32, \u0026.{ 1, null, 3, null });\nit.next(); // 1\nit.next(); // 3\n```\n\n### compactAlloc\n\nRemove zero/null/default values into an allocated slice. Caller owns the returned slice.\n\n```zig\nconst result = try lo.compactAlloc(?i32, allocator, \u0026.{ 1, null, 3 });\ndefer allocator.free(result);\n```\n\n### flatten\n\nFlatten a slice of slices into a single sequence. Returns a lazy iterator.\n\n```zig\nconst data = [_][]const i32{ \u0026.{ 1, 2 }, \u0026.{ 3, 4 } };\nvar it = lo.flatten(i32, \u0026data);\n// yields 1, 2, 3, 4\n```\n\n### flattenAlloc\n\nFlatten a slice of slices into an allocated slice. Counts total elements first, then allocates once. Caller owns the returned slice.\n\n```zig\nconst data = [_][]const i32{ \u0026.{ 1, 2 }, \u0026.{ 3, 4, 5 } };\nconst result = try lo.flattenAlloc(i32, allocator, \u0026data);\ndefer allocator.free(result);\n// result == \u0026.{ 1, 2, 3, 4, 5 }\n```\n\n### flattenDeep\n\nFlatten two levels of nesting (`[][][]T` to `[]T`). Caller owns the returned slice.\n\n```zig\nconst inner = [_][]const i32{ \u0026.{ 1, 2 }, \u0026.{ 3 } };\nconst outer = [_][]const []const i32{ \u0026inner };\nconst result = try lo.flattenDeep(i32, allocator, \u0026outer);\ndefer allocator.free(result);\n// result == \u0026.{ 1, 2, 3 }\n```\n\n### flatMap\n\nMap each element to a slice, then flatten into a single sequence. Returns a lazy iterator.\n\n```zig\nvar it = lo.flatMap(i32, u8, \u0026.{ 1, 2 }, toDigits);\n```\n\n### flatMapAlloc\n\nMap then flatten, collected into an allocated slice. Caller owns the returned slice.\n\n```zig\nconst result = try lo.flatMapAlloc(i32, u8, allocator, \u0026.{ 1, 2 }, toDigits);\ndefer allocator.free(result);\n```\n\n### without\n\nExclude specific values from a slice. Returns a lazy iterator.\n\n```zig\nvar it = lo.without(i32, \u0026.{ 1, 2, 3, 4 }, \u0026.{ 2, 4 });\n// yields 1, 3\n```\n\n### forEach\n\nInvoke a function on each element.\n\n```zig\nlo.forEach(i32, \u0026.{ 1, 2, 3 }, printFn);\n```\n\n### forEachIndex\n\nInvoke a function on each element with its index.\n\n```zig\nlo.forEachIndex(i32, \u0026.{ 10, 20 }, printWithIndex);\n```\n\n### compactMap\n\nFilter and map in a single pass. The transform returns `?R`; non-null values are collected into an allocated slice. Caller owns the returned slice.\n\n```zig\nconst toEvenDoubled = struct {\n    fn f(x: i32) ?i32 {\n        if (@mod(x, 2) == 0) return x * 2;\n        return null;\n    }\n}.f;\nconst result = try lo.compactMap(i32, i32, allocator, \u0026.{ 1, 2, 3, 4 }, toEvenDoubled);\ndefer allocator.free(result);\n// result == \u0026.{ 4, 8 }\n```\n\n### filterMapIter\n\nFilter and map in a single step. Returns a lazy iterator.\n\n```zig\nvar it = lo.filterMapIter(i32, i32, \u0026.{ 1, 2, 3, 4 }, toEvenDoubled);\nit.next(); // 4\nit.next(); // 8\nit.next(); // null\n```\n\n---\n\n## Aggregate\n\n### reduce\n\nLeft fold with an accumulator.\n\n```zig\nlo.reduce(i32, i32, \u0026.{ 1, 2, 3 }, addFn, 0); // 6\n```\n\n### reduceRight\n\nRight fold with an accumulator. Processes elements right to left.\n\n```zig\nlo.reduceRight(i32, i32, \u0026.{ 1, 2, 3 }, subtractFn, 0);\n```\n\n### sum\n\nSum all elements in a slice. Returns 0 for empty slices.\n\n```zig\nlo.sum(i32, \u0026.{ 1, 2, 3, 4 }); // 10\n```\n\n### sumBy\n\nSum elements after applying a transform function.\n\n```zig\nlo.sumBy(i32, i64, \u0026.{ 1, 2, 3 }, double); // 12\n```\n\n### product\n\nMultiply all elements in a slice. Returns 1 for empty slices.\n\n```zig\nlo.product(i32, \u0026.{ 2, 3, 4 }); // 24\n```\n\n### productBy\n\nMultiply elements after applying a transform function.\n\n```zig\nlo.productBy(i32, i64, \u0026.{ 2, 3, 4 }, double); // 192\n```\n\n### mean\n\nArithmetic mean of a slice. Returns null for empty slices.\n\n```zig\nlo.mean(i32, \u0026.{ 2, 4, 6 }).?; // 4.0\n```\n\n### meanBy\n\nArithmetic mean after applying a transform function.\n\n```zig\nconst asF64 = struct { fn f(x: i32) f64 { return @floatFromInt(x); } }.f;\nlo.meanBy(i32, \u0026vals, asF64).?; // 20.0\n```\n\n### min\n\nReturns the minimum value in a slice, or null if empty.\n\n```zig\nlo.min(i32, \u0026.{ 3, 1, 2 }); // 1\n```\n\n### max\n\nReturns the maximum value in a slice, or null if empty.\n\n```zig\nlo.max(i32, \u0026.{ 3, 1, 2 }); // 3\n```\n\n### minBy\n\nReturns the minimum element according to a comparator.\n\n```zig\nlo.minBy(Point, \u0026points, Point.compareByX); // point with smallest x\n```\n\n### maxBy\n\nReturns the maximum element according to a comparator.\n\n```zig\nlo.maxBy(Point, \u0026points, Point.compareByX); // point with largest x\n```\n\n### minMax\n\nReturns both min and max in a single pass. Null if empty.\n\n```zig\nconst mm = lo.minMax(i32, \u0026.{ 5, 1, 9, 3 }).?;\n// mm.min_val == 1, mm.max_val == 9\n```\n\n### minMaxBy\n\nReturns both min and max in a single pass according to a custom comparator. Null if empty.\n\n```zig\nconst byX = struct { fn f(a: Point, b: Point) std.math.Order {\n    return std.math.order(a.x, b.x);\n} }.f;\nconst mm = lo.minMaxBy(Point, \u0026points, byX).?;\n// mm.min_val and mm.max_val\n```\n\n### count\n\nCount elements satisfying the predicate.\n\n```zig\nlo.count(i32, \u0026.{ 1, 2, 3, 4 }, isEven); // 2\n```\n\n### countBy\n\nCount elements by a key function. Returns a frequency map.\n\n```zig\nvar m = try lo.countBy(i32, bool, allocator, \u0026.{ 1, 2, 3, 4, 5 }, isEvenFn);\ndefer m.deinit();\nm.get(true).?;  // 2\nm.get(false).?; // 3\n```\n\n### countValues\n\nBuild a frequency map: value -\u003e number of occurrences. Caller owns the returned map.\n\n```zig\nvar freq = try lo.countValues(i32, allocator, \u0026.{ 1, 2, 2, 3 });\ndefer freq.deinit();\nfreq.get(2).?; // 2\n```\n\n### mode\n\nReturns the most frequently occurring value. Smallest value wins on ties. Null for empty slices.\n\n```zig\nconst m = try lo.mode(i32, allocator, \u0026.{ 1, 2, 2, 3, 2 });\n// m == 2\n```\n\n### median\n\nReturns the median of a numeric slice, or null if empty. Allocates a temporary copy for sorting.\n\n```zig\nconst m = try lo.median(i32, allocator, \u0026.{ 1, 2, 3, 4 });\n// m == 2.5\n```\n\n### variance\n\nPopulation variance (N denominator). Returns null for empty slices.\n\n```zig\nlo.variance(i32, \u0026.{ 2, 4, 4, 4, 5, 5, 7, 9 }); // 4.0\n```\n\n### stddev\n\nStandard deviation (sqrt of population variance). Returns null for empty slices.\n\n```zig\nlo.stddev(i32, \u0026.{ 2, 4, 4, 4, 5, 5, 7, 9 }); // 2.0\n```\n\n### sampleVariance\n\nSample variance with N-1 denominator (Bessel's correction). Returns null for slices with fewer than 2 elements.\n\n```zig\nlo.sampleVariance(i32, \u0026.{ 2, 4, 4, 4, 5, 5, 7, 9 }); // ~4.571\n```\n\n### sampleStddev\n\nSample standard deviation (sqrt of sample variance). Returns null for slices with fewer than 2 elements.\n\n```zig\nlo.sampleStddev(i32, \u0026.{ 2, 4, 4, 4, 5, 5, 7, 9 }); // ~2.138\n```\n\n### percentile\n\nReturns the nth percentile using linear interpolation. Null for empty slices or if p is outside [0, 100].\n\n```zig\nconst p = try lo.percentile(i32, allocator, \u0026.{ 1, 2, 3, 4, 5 }, 50.0);\n// p == 3.0\n```\n\n---\n\n## Sort \u0026 Order\n\n### sortBy\n\nSort a slice in-place by a key extracted via a function. Stable sort.\n\n```zig\nvar items = [_]i32{ 30, 10, 20 };\nlo.sortBy(i32, i32, \u0026items, struct {\n    fn f(x: i32) i32 { return x; }\n}.f);\n// items == { 10, 20, 30 }\n```\n\n### sortByAlloc\n\nReturns a sorted copy without mutating the original. Caller owns the returned slice.\n\n```zig\nconst sorted = try lo.sortByAlloc(i32, i32, allocator, \u0026.{ 30, 10, 20 }, struct {\n    fn f(x: i32) i32 { return x; }\n}.f);\ndefer allocator.free(sorted);\n// sorted == { 10, 20, 30 }\n```\n\n### sortByField\n\nSort a slice of structs in-place by a named field. Stable sort.\n\n```zig\nconst Person = struct { name: []const u8, age: u32 };\nvar items = [_]Person{ .{ .name = \"bob\", .age = 30 }, .{ .name = \"alice\", .age = 25 } };\nlo.sortByField(Person, \u0026items, .age);\n// items[0].age == 25, items[1].age == 30\n```\n\n### sortByFieldAlloc\n\nReturns a sorted copy of the slice sorted by a named struct field. Caller owns the returned slice.\n\n```zig\nconst sorted = try lo.sortByFieldAlloc(Person, allocator, \u0026people, .age);\ndefer allocator.free(sorted);\n```\n\n### toSortedAlloc\n\nReturns a sorted copy in natural ascending order. Caller owns the returned slice.\n\n```zig\nconst sorted = try lo.toSortedAlloc(i32, allocator, \u0026.{ 3, 1, 2 });\ndefer allocator.free(sorted);\n// sorted == { 1, 2, 3 }\n```\n\n### isSorted\n\nTrue if the slice is sorted according to the comparator.\n\n```zig\nlo.isSorted(i32, \u0026.{ 1, 2, 3 }, compareAsc); // true\n```\n\n### equal\n\nElement-wise equality of two slices.\n\n```zig\nlo.equal(i32, \u0026.{ 1, 2, 3 }, \u0026.{ 1, 2, 3 }); // true\n```\n\n### reverse\n\nReverse a slice in-place.\n\n```zig\nvar data = [_]i32{ 1, 2, 3 };\nlo.reverse(i32, \u0026data);\n// data == .{ 3, 2, 1 }\n```\n\n### shuffle\n\nFisher-Yates shuffle in-place.\n\n```zig\nvar data = [_]i32{ 1, 2, 3, 4, 5 };\nlo.shuffle(i32, \u0026data, prng.random());\n```\n\n---\n\n## Set Operations\n\n### uniq\n\nRemove duplicate elements. Preserves first occurrence order.\n\n```zig\nconst u = try lo.uniq(i32, allocator, \u0026.{ 1, 2, 2, 3, 1 });\ndefer allocator.free(u);\n// u == \u0026.{ 1, 2, 3 }\n```\n\n### uniqBy\n\nRemove duplicates by a key function. Preserves first occurrence order.\n\n```zig\nconst u = try lo.uniqBy(Person, u32, allocator, \u0026people, Person.id);\ndefer allocator.free(u);\n```\n\n### intersect\n\nElements present in both slices. Order follows the first slice.\n\n```zig\nconst i = try lo.intersect(i32, allocator, \u0026.{ 1, 2, 3 }, \u0026.{ 2, 3, 4 });\ndefer allocator.free(i);\n// i == \u0026.{ 2, 3 }\n```\n\n### union\\_\n\nUnique elements from both slices combined.\n\n```zig\nconst u = try lo.union_(i32, allocator, \u0026.{ 1, 2, 3 }, \u0026.{ 2, 3, 4 });\ndefer allocator.free(u);\n// u == \u0026.{ 1, 2, 3, 4 }\n```\n\n### difference\n\nElements in the first slice but not in the second.\n\n```zig\nconst d = try lo.difference(i32, allocator, \u0026.{ 1, 2, 3 }, \u0026.{ 2, 4 });\ndefer allocator.free(d);\n// d == \u0026.{ 1, 3 }\n```\n\n### symmetricDifference\n\nElements in either slice but not in both.\n\n```zig\nconst sd = try lo.symmetricDifference(i32, allocator, \u0026.{ 1, 2, 3 }, \u0026.{ 2, 3, 4 });\ndefer allocator.free(sd);\n// sd == \u0026.{ 1, 4 }\n```\n\n### findDuplicates\n\nFind elements appearing more than once. Preserves first-occurrence order.\n\n```zig\nconst dups = try lo.findDuplicates(i32, allocator, \u0026.{ 1, 2, 2, 3, 3, 3 });\ndefer allocator.free(dups);\n// dups == \u0026.{ 2, 3 }\n```\n\n### findUniques\n\nFind elements appearing exactly once.\n\n```zig\nconst uniques = try lo.findUniques(i32, allocator, \u0026.{ 1, 2, 2, 3, 3, 3, 4 });\ndefer allocator.free(uniques);\n// uniques == \u0026.{ 1, 4 }\n```\n\n### elementsMatch\n\nTrue if two slices contain the same elements with the same multiplicities, regardless of order.\n\n```zig\ntry lo.elementsMatch(i32, allocator, \u0026.{ 1, 2, 3 }, \u0026.{ 3, 2, 1 }); // true\ntry lo.elementsMatch(i32, allocator, \u0026.{ 1, 1, 2 }, \u0026.{ 1, 2, 2 }); // false\n```\n\n### differenceWith\n\nElements in the first slice but not in the second, using a custom equality predicate.\n\n```zig\nconst absEq = struct { fn f(a: i32, b: i32) bool { return @abs(a) == @abs(b); } }.f;\nconst d = try lo.differenceWith(i32, allocator, \u0026.{ 1, 2, 3 }, \u0026.{ -2, 4 }, absEq);\ndefer allocator.free(d);\n// d == \u0026.{ 1, 3 }\n```\n\n### intersectWith\n\nElements present in both slices, using a custom equality predicate.\n\n```zig\nconst i = try lo.intersectWith(i32, allocator, \u0026.{ 1, -2, 3 }, \u0026.{ 2, 4 }, absEq);\ndefer allocator.free(i);\n// i == \u0026.{ -2 }\n```\n\n### unionWith\n\nUnique elements from both slices combined, using a custom equality predicate.\n\n```zig\nconst u = try lo.unionWith(i32, allocator, \u0026.{ 1, 2 }, \u0026.{ -2, 3 }, absEq);\ndefer allocator.free(u);\n// u == \u0026.{ 1, 2, 3 }\n```\n\n---\n\n## Partition \u0026 Group\n\n### partition\n\nSplit a slice into two: elements matching the predicate and the rest.\n\n```zig\nconst p = try lo.partition(i32, allocator, \u0026.{ 1, 2, 3, 4 }, isEven);\ndefer p.deinit(allocator);\n// p.matching == \u0026.{ 2, 4 }, p.rest == \u0026.{ 1, 3 }\n```\n\n### groupBy\n\nGroup elements by a key function. Caller owns the returned map.\n\n```zig\nvar groups = try lo.groupBy(i32, bool, allocator, \u0026.{ 1, 2, 3, 4 }, isEvenFn);\ndefer {\n    var it = groups.valueIterator();\n    while (it.next()) |list| list.deinit(allocator);\n    groups.deinit();\n}\n```\n\n### chunk\n\nSplit a slice into chunks of the given size. The last chunk may be smaller. Returns a lazy iterator.\n\n```zig\nvar it = lo.chunk(i32, \u0026.{ 1, 2, 3, 4, 5 }, 2);\nit.next(); // \u0026.{ 1, 2 }\nit.next(); // \u0026.{ 3, 4 }\nit.next(); // \u0026.{ 5 }\n```\n\n### window\n\nSliding window over a slice. Returns a lazy iterator. Windows borrow from the input (zero allocation).\n\n```zig\nvar it = lo.window(i32, \u0026.{ 1, 2, 3, 4, 5 }, 3);\nit.next(); // \u0026.{ 1, 2, 3 }\nit.next(); // \u0026.{ 2, 3, 4 }\nit.next(); // \u0026.{ 3, 4, 5 }\nit.next(); // null\n```\n\n### scan\n\nLike reduce but emits every intermediate result. Returns a lazy iterator.\n\n```zig\nconst add = struct { fn f(a: i32, b: i32) i32 { return a + b; } }.f;\nvar it = lo.scan(i32, i32, \u0026.{ 1, 2, 3 }, add, 0);\nit.next(); // 1\nit.next(); // 3\nit.next(); // 6\n```\n\n### scanAlloc\n\nEagerly compute all intermediate accumulator values into an allocated slice. Caller owns the returned slice.\n\n```zig\nconst add = struct { fn f(a: i32, b: i32) i32 { return a + b; } }.f;\nconst result = try lo.scanAlloc(i32, i32, allocator, \u0026.{ 1, 2, 3 }, add, 0);\ndefer allocator.free(result);\n// result == \u0026.{ 1, 3, 6 }\n```\n\n---\n\n## Combine\n\n### concat\n\nConcatenate multiple slices into a single allocated slice. Caller owns the returned slice.\n\n```zig\nconst result = try lo.concat(i32, allocator, \u0026.{ \u0026.{ 1, 2 }, \u0026.{ 3, 4 }, \u0026.{5} });\ndefer allocator.free(result);\n// result == { 1, 2, 3, 4, 5 }\n```\n\n### splice\n\nInsert elements into a slice at a given index, returning a new allocated slice. Caller owns the returned slice.\n\n```zig\nconst result = try lo.splice(i32, allocator, \u0026.{ 1, 2, 5, 6 }, 2, \u0026.{ 3, 4 });\ndefer allocator.free(result);\n// result == { 1, 2, 3, 4, 5, 6 }\n```\n\n### interleave\n\nRound-robin interleave multiple slices. Returns a lazy iterator.\n\n```zig\nvar it = lo.interleave(i32, \u0026.{ \u0026.{ 1, 2, 3 }, \u0026.{ 4, 5, 6 } });\nit.next(); // 1\nit.next(); // 4\nit.next(); // 2\nit.next(); // 5\n```\n\n### fill\n\nFill all elements with the given value (in-place).\n\n```zig\nvar data = [_]i32{ 0, 0, 0 };\nlo.fill(i32, \u0026data, 42);\n// data == .{ 42, 42, 42 }\n```\n\n### fillRange\n\nFill elements in the range [start, end) with the given value (in-place).\n\n```zig\nvar data = [_]i32{ 1, 2, 3, 4, 5 };\nlo.fillRange(i32, \u0026data, 0, 1, 4);\n// data == .{ 1, 0, 0, 0, 5 }\n```\n\n### repeat\n\nCreate a slice of n copies of a value. Caller owns the returned slice.\n\n```zig\nconst r = try lo.repeat(i32, allocator, 42, 3);\ndefer allocator.free(r);\n// r == \u0026.{ 42, 42, 42 }\n```\n\n### repeatBy\n\nCreate a slice of n elements produced by a callback. Caller owns the returned slice.\n\n```zig\nconst r = try lo.repeatBy(i32, allocator, 3, indexSquared);\ndefer allocator.free(r);\n// r == \u0026.{ 0, 1, 4 }\n```\n\n### times\n\nCreate a lazy iterator that calls a function N times with indices 0..N-1.\n\n```zig\nvar iter = lo.times(usize, 4, square);\nwhile (iter.next()) |val| { ... }\n```\n\n### timesAlloc\n\nEagerly call a function N times and return the results. Caller owns the returned slice.\n\n```zig\nconst squares = try lo.timesAlloc(usize, allocator, 4, square);\ndefer allocator.free(squares);\n// squares == { 0, 1, 4, 9 }\n```\n\n---\n\n## Search\n\n### find\n\nFirst element matching the predicate, or null.\n\n```zig\nlo.find(i32, \u0026.{ 1, 2, 3, 4 }, isEven); // 2\n```\n\n### findIndex\n\nIndex of the first element matching the predicate, or null.\n\n```zig\nlo.findIndex(i32, \u0026.{ 1, 2, 3 }, isEven); // 1\n```\n\n### findLast\n\nLast element matching the predicate, or null.\n\n```zig\nlo.findLast(i32, \u0026.{ 1, 2, 3, 4 }, isEven); // 4\n```\n\n### findLastIndex\n\nIndex of the last element matching the predicate, or null.\n\n```zig\nlo.findLastIndex(i32, \u0026.{ 1, 2, 3, 4 }, isEven); // 3\n```\n\n### indexOf\n\nIndex of the first occurrence of a value, or null.\n\n```zig\nlo.indexOf(i32, \u0026.{ 10, 20, 30 }, 20); // 1\n```\n\n### lastIndexOf\n\nIndex of the last occurrence of a value, or null.\n\n```zig\nlo.lastIndexOf(i32, \u0026.{ 1, 2, 3, 2 }, 2); // 3\n```\n\n### contains\n\nTrue if the slice contains the given value.\n\n```zig\nlo.contains(i32, \u0026.{ 1, 2, 3 }, 2); // true\n```\n\n### containsBy\n\nTrue if any element satisfies the predicate.\n\n```zig\nlo.containsBy(i32, \u0026.{ 1, 2, 3 }, isEven); // true\n```\n\n### every\n\nTrue if all elements satisfy the predicate. True for empty slices.\n\n```zig\nlo.every(i32, \u0026.{ 2, 4, 6 }, isEven); // true\n```\n\n### some\n\nTrue if at least one element satisfies the predicate. False for empty slices.\n\n```zig\nlo.some(i32, \u0026.{ 1, 2, 3 }, isEven); // true\n```\n\n### none\n\nTrue if no elements satisfy the predicate. True for empty slices.\n\n```zig\nlo.none(i32, \u0026.{ 1, 3, 5 }, isEven); // true\n```\n\n### minIndex\n\nReturns the index of the minimum element, or null if empty. First occurrence on ties.\n\n```zig\nlo.minIndex(i32, \u0026.{ 3, 1, 4, 1, 5 }); // 1\n```\n\n### maxIndex\n\nReturns the index of the maximum element, or null if empty. First occurrence on ties.\n\n```zig\nlo.maxIndex(i32, \u0026.{ 3, 1, 4, 1, 5 }); // 4\n```\n\n### binarySearch\n\nBinary search for a target in a sorted ascending slice. Returns the index or null. O(log n).\n\n```zig\nlo.binarySearch(i32, \u0026.{ 1, 3, 5, 7, 9 }, 5); // Some(2)\nlo.binarySearch(i32, \u0026.{ 1, 3, 5, 7, 9 }, 4); // null\n```\n\n### lowerBound\n\nIndex of the first element \u003e= target in a sorted slice. Returns `slice.len` if all are less.\n\n```zig\nlo.lowerBound(i32, \u0026.{ 1, 3, 5, 7 }, 4); // 2\nlo.lowerBound(i32, \u0026.{ 1, 3, 5, 7 }, 5); // 2\nlo.lowerBound(i32, \u0026.{ 1, 3, 5, 7 }, 9); // 4\n```\n\n### upperBound\n\nIndex of the first element \u003e target in a sorted slice. Returns `slice.len` if all are \u003c= target.\n\n```zig\nlo.upperBound(i32, \u0026.{ 1, 3, 5, 7 }, 3); // 2\nlo.upperBound(i32, \u0026.{ 1, 3, 5, 7 }, 4); // 2\nlo.upperBound(i32, \u0026.{ 1, 3, 5, 7 }, 9); // 4\n```\n\n### sortedIndex\n\nInsertion index to maintain sorted order. Equivalent to `lowerBound`.\n\n```zig\nlo.sortedIndex(i32, \u0026.{ 1, 3, 5, 7 }, 4); // 2\n```\n\n### sortedLastIndex\n\nInsertion index after the last occurrence of a value. Equivalent to `upperBound`.\n\n```zig\nlo.sortedLastIndex(i32, \u0026.{ 1, 3, 3, 5 }, 3); // 3\n```\n\n---\n\n## Map Helpers\n\n### keys\n\nIterate over map keys. Returns a lazy iterator.\n\n```zig\nvar it = lo.keys(u32, u8, \u0026my_map);\nwhile (it.next()) |key| { ... }\n```\n\n### keysAlloc\n\nCollect all keys into an allocated slice. Caller owns the returned slice.\n\n```zig\nconst ks = try lo.keysAlloc(u32, u8, allocator, \u0026my_map);\ndefer allocator.free(ks);\n```\n\n### values\n\nIterate over map values. Returns a lazy iterator.\n\n```zig\nvar it = lo.values(u32, u8, \u0026my_map);\nwhile (it.next()) |val| { ... }\n```\n\n### valuesAlloc\n\nCollect all values into an allocated slice. Caller owns the returned slice.\n\n```zig\nconst vs = try lo.valuesAlloc(u32, u8, allocator, \u0026my_map);\ndefer allocator.free(vs);\n```\n\n### entries\n\nIterate over key-value pairs. Returns a lazy iterator.\n\n```zig\nvar it = lo.entries(u32, u8, \u0026my_map);\nwhile (it.next()) |e| { _ = e.key; _ = e.value; }\n```\n\n### entriesAlloc\n\nCollect all key-value pairs into an allocated slice. Caller owns the returned slice.\n\n```zig\nconst es = try lo.entriesAlloc(u32, u8, allocator, \u0026my_map);\ndefer allocator.free(es);\n```\n\n### fromEntries\n\nBuild a map from a slice of key-value pairs. Caller owns the returned map.\n\n```zig\nconst pairs = [_]lo.Entry(u32, u8){ .{ .key = 1, .value = 'a' } };\nvar m = try lo.fromEntries(u32, u8, allocator, \u0026pairs);\ndefer m.deinit();\n```\n\n### mapKeys\n\nTransform map keys using a function. Caller owns the returned map.\n\n```zig\nvar result = try lo.mapKeys(u32, u8, u64, allocator, \u0026m, timesTwo);\ndefer result.deinit();\n```\n\n### mapValues\n\nTransform map values using a function. Caller owns the returned map.\n\n```zig\nvar result = try lo.mapValues(u32, u8, u16, allocator, \u0026m, multiply);\ndefer result.deinit();\n```\n\n### filterMap\n\nFilter map entries by a predicate on key and value. Caller owns the returned map.\n\n```zig\nvar result = try lo.filterMap(u32, u8, allocator, \u0026m, keyGt1);\ndefer result.deinit();\n```\n\n### filterKeys\n\nFilter map entries by a predicate on the key. Caller owns the returned map.\n\n```zig\nvar result = try lo.filterKeys(u32, u8, allocator, \u0026m, isEven);\ndefer result.deinit();\n```\n\n### filterValues\n\nFilter map entries by a predicate on the value. Caller owns the returned map.\n\n```zig\nvar result = try lo.filterValues(u32, u8, allocator, \u0026m, isPositive);\ndefer result.deinit();\n```\n\n### pickKeys\n\nKeep only entries with the specified keys. Caller owns the returned map.\n\n```zig\nvar result = try lo.pickKeys(u32, u8, allocator, \u0026m, \u0026.{ 1, 3 });\ndefer result.deinit();\n```\n\n### omitKeys\n\nRemove entries with the specified keys. Caller owns the returned map.\n\n```zig\nvar result = try lo.omitKeys(u32, u8, allocator, \u0026m, \u0026.{ 2, 3 });\ndefer result.deinit();\n```\n\n### invert\n\nSwap keys and values. Caller owns the returned map.\n\n```zig\nvar result = try lo.invert(u32, u8, allocator, \u0026m);\ndefer result.deinit();\n```\n\n### merge\n\nMerge entries from source into dest. Source values overwrite on conflict.\n\n```zig\ntry lo.merge(u32, u8, \u0026dest, \u0026source);\n```\n\n### assign\n\nMerge N maps into one with last-write-wins semantics. Caller owns the returned map.\n\n```zig\nvar result = try lo.assign(u32, u8, allocator, \u0026.{ \u0026m1, \u0026m2 });\ndefer result.deinit();\n```\n\n### mapEntries\n\nTransform both keys and values of a map. Caller owns the returned map.\n\n```zig\nvar result = try lo.mapEntries(u32, u8, u64, u16, allocator, \u0026m, xform);\ndefer result.deinit();\n```\n\n### mapToSlice\n\nTransform map entries into an allocated slice. Caller owns the returned slice.\n\n```zig\nconst result = try lo.mapToSlice(u32, u8, u64, allocator, \u0026m, sumKeyVal);\ndefer allocator.free(result);\n```\n\n### valueOr\n\nGet a value from the map, or return a default if the key is absent.\n\n```zig\nlo.valueOr(u32, u8, \u0026my_map, 999, 0); // 0 if 999 not in map\n```\n\n### hasKey\n\nTrue if the map contains the given key.\n\n```zig\nlo.hasKey(u32, u8, \u0026m, 1); // true\n```\n\n### mapCount\n\nNumber of entries in the map.\n\n```zig\nlo.mapCount(u32, u8, \u0026m); // 3\n```\n\n### keyBy\n\nConvert a slice to a map indexed by an extracted key. Last element wins on duplicate keys.\n\n```zig\nvar m = try lo.keyBy(Person, u32, allocator, \u0026people, getAge);\ndefer m.deinit();\n```\n\n### associate\n\nConvert a slice to a map with custom key and value extraction. Last element wins on duplicate keys.\n\n```zig\nvar m = try lo.associate(Person, u32, []const u8, allocator, \u0026people, toEntry);\ndefer m.deinit();\n```\n\n---\n\n## String Helpers\n\n### words\n\nSplit a string into words at camelCase, PascalCase, snake\\_case, kebab-case, and whitespace boundaries. Returns a lazy iterator.\n\n```zig\nvar it = lo.words(\"helloWorld\");\nit.next(); // \"hello\"\nit.next(); // \"World\"\n```\n\n### wordsAlloc\n\nSplit a string into words, collected into an allocated slice. Caller owns the returned slice.\n\n```zig\nconst ws = try lo.wordsAlloc(allocator, \"camelCase\");\ndefer allocator.free(ws);\n// ws: \u0026.{ \"camel\", \"Case\" }\n```\n\n### camelCase\n\nConvert a string to camelCase. Caller owns the returned string.\n\n```zig\nconst s = try lo.camelCase(allocator, \"hello_world\");\ndefer allocator.free(s);\n// s == \"helloWorld\"\n```\n\n### pascalCase\n\nConvert a string to PascalCase. Caller owns the returned string.\n\n```zig\nconst s = try lo.pascalCase(allocator, \"hello_world\");\ndefer allocator.free(s);\n// s == \"HelloWorld\"\n```\n\n### snakeCase\n\nConvert a string to snake\\_case. Caller owns the returned string.\n\n```zig\nconst s = try lo.snakeCase(allocator, \"helloWorld\");\ndefer allocator.free(s);\n// s == \"hello_world\"\n```\n\n### kebabCase\n\nConvert a string to kebab-case. Caller owns the returned string.\n\n```zig\nconst s = try lo.kebabCase(allocator, \"helloWorld\");\ndefer allocator.free(s);\n// s == \"hello-world\"\n```\n\n### capitalize\n\nCapitalize the first letter of a string. Caller owns the returned string.\n\n```zig\nconst s = try lo.capitalize(allocator, \"hello\");\ndefer allocator.free(s);\n// s == \"Hello\"\n```\n\n### lowerFirst\n\nLowercase just the first character (ASCII). Caller owns the returned string.\n\n```zig\nconst s = try lo.lowerFirst(allocator, \"Hello\");\ndefer allocator.free(s);\n// s == \"hello\"\n```\n\n### toLower\n\nConvert an entire string to lowercase (ASCII). Caller owns the returned string.\n\n```zig\nconst s = try lo.toLower(allocator, \"Hello World\");\ndefer allocator.free(s);\n// s == \"hello world\"\n```\n\n### toUpper\n\nConvert an entire string to uppercase (ASCII). Caller owns the returned string.\n\n```zig\nconst s = try lo.toUpper(allocator, \"Hello World\");\ndefer allocator.free(s);\n// s == \"HELLO WORLD\"\n```\n\n### trim\n\nTrim whitespace from both ends of a string. Returns a sub-slice (zero-copy).\n\n```zig\nlo.trim(\"  hello  \"); // \"hello\"\n```\n\n### trimStart\n\nTrim whitespace from the start of a string. Returns a sub-slice (zero-copy).\n\n```zig\nlo.trimStart(\"  hello  \"); // \"hello  \"\n```\n\n### trimEnd\n\nTrim whitespace from the end of a string. Returns a sub-slice (zero-copy).\n\n```zig\nlo.trimEnd(\"  hello  \"); // \"  hello\"\n```\n\n### startsWith\n\nCheck if a string starts with a given prefix.\n\n```zig\nlo.startsWith(\"hello world\", \"hello\"); // true\n```\n\n### endsWith\n\nCheck if a string ends with a given suffix.\n\n```zig\nlo.endsWith(\"hello world\", \"world\"); // true\n```\n\n### includes\n\nCheck if a string contains a substring.\n\n```zig\nlo.includes(\"hello world\", \"world\"); // true\n```\n\n### substr\n\nExtract a substring by start and end byte indices. Indices are clamped. Returns a sub-slice (zero-copy).\n\n```zig\nlo.substr(\"hello\", 2, 5); // \"llo\"\n```\n\n### ellipsis\n\nTruncate a string and add \"...\" if it exceeds max\\_len. Caller owns the returned string.\n\n```zig\nconst s = try lo.ellipsis(allocator, \"hello world\", 8);\ndefer allocator.free(s);\n// s == \"hello...\"\n```\n\n### strRepeat\n\nRepeat a string n times. Caller owns the returned string.\n\n```zig\nconst s = try lo.strRepeat(allocator, \"ab\", 3);\ndefer allocator.free(s);\n// s == \"ababab\"\n```\n\n### padLeft\n\nLeft-pad a string to the given length. Caller owns the returned string.\n\n```zig\nconst s = try lo.padLeft(allocator, \"42\", 5, '0');\ndefer allocator.free(s);\n// s == \"00042\"\n```\n\n### padRight\n\nRight-pad a string to the given length. Caller owns the returned string.\n\n```zig\nconst s = try lo.padRight(allocator, \"hi\", 5, '.');\ndefer allocator.free(s);\n// s == \"hi...\"\n```\n\n### runeLength\n\nCount the number of Unicode codepoints in a UTF-8 string. Returns `error.InvalidUtf8` for invalid input.\n\n```zig\ntry lo.runeLength(\"hello\");     // 5\ntry lo.runeLength(\"\\xc3\\xa9\");  // 1\n```\n\n### randomString\n\nGenerate a random alphanumeric string. Caller owns the returned string.\n\n```zig\nconst s = try lo.randomString(allocator, 10, prng.random());\ndefer allocator.free(s);\n```\n\n### split\n\nSplit a string by a delimiter sequence. Returns a lazy iterator. Preserves empty tokens.\n\n```zig\nvar it = lo.split(\"one,two,,four\", \",\");\nit.next(); // \"one\"\nit.next(); // \"two\"\nit.next(); // \"\"\nit.next(); // \"four\"\n```\n\n### splitAlloc\n\nSplit a string by a delimiter, collected into an allocated slice. Caller owns the returned outer slice.\n\n```zig\nconst parts = try lo.splitAlloc(allocator, \"a-b-c\", \"-\");\ndefer allocator.free(parts);\n// parts[0] == \"a\", parts[1] == \"b\", parts[2] == \"c\"\n```\n\n### join\n\nJoin a slice of strings with a separator. Caller owns the returned string.\n\n```zig\nconst s = try lo.join(allocator, \", \", \u0026.{ \"hello\", \"world\" });\ndefer allocator.free(s);\n// s == \"hello, world\"\n```\n\n### replace\n\nReplace the first occurrence of a needle. Caller owns the returned string.\n\n```zig\nconst s = try lo.replace(allocator, \"hello hello\", \"hello\", \"hi\");\ndefer allocator.free(s);\n// s == \"hi hello\"\n```\n\n### replaceAll\n\nReplace all occurrences of a needle. Caller owns the returned string.\n\n```zig\nconst s = try lo.replaceAll(allocator, \"hello hello\", \"hello\", \"hi\");\ndefer allocator.free(s);\n// s == \"hi hi\"\n```\n\n### chunkString\n\nSplit a string into fixed-size byte chunks. Returns a lazy iterator. The last chunk may be smaller.\n\n```zig\nvar it = lo.chunkString(\"abcdefgh\", 3);\nit.next(); // \"abc\"\nit.next(); // \"def\"\nit.next(); // \"gh\"\n```\n\n---\n\n## Math\n\n### sum\n\nSum all elements. Returns 0 for empty slices.\n\n```zig\nlo.sum(i32, \u0026.{ 1, 2, 3, 4 }); // 10\n```\n\n### mean\n\nArithmetic mean. Returns null for empty slices.\n\n```zig\nlo.mean(i32, \u0026.{ 2, 4, 6 }).?; // 4.0\n```\n\n### median\n\nMedian value. Allocates a temporary sorted copy. Null for empty slices.\n\n```zig\nconst m = try lo.median(i32, allocator, \u0026.{ 1, 2, 3, 4 });\n// m == 2.5\n```\n\n### variance\n\nPopulation variance (N denominator). Null for empty slices.\n\n```zig\nlo.variance(i32, \u0026.{ 2, 4, 4, 4, 5, 5, 7, 9 }); // 4.0\n```\n\n### stddev\n\nStandard deviation (sqrt of population variance). Null for empty slices.\n\n```zig\nlo.stddev(i32, \u0026.{ 2, 4, 4, 4, 5, 5, 7, 9 }); // 2.0\n```\n\n### sampleVariance\n\nSample variance with N-1 denominator (Bessel's correction). Null for slices with fewer than 2 elements.\n\n```zig\nlo.sampleVariance(i32, \u0026.{ 2, 4, 4, 4, 5, 5, 7, 9 }); // ~4.571\n```\n\n### sampleStddev\n\nSample standard deviation (sqrt of sample variance). Null for slices with fewer than 2 elements.\n\n```zig\nlo.sampleStddev(i32, \u0026.{ 2, 4, 4, 4, 5, 5, 7, 9 }); // ~2.138\n```\n\n### percentile\n\nNth percentile using linear interpolation. Null for empty slices or p outside [0, 100].\n\n```zig\nconst p = try lo.percentile(i32, allocator, \u0026.{ 1, 2, 3, 4, 5 }, 50.0);\n// p == 3.0\n```\n\n### lerp\n\nLinear interpolation between two values. Float-only.\n\n```zig\nlo.lerp(f64, 0.0, 10.0, 0.5); // 5.0\n```\n\n### remap\n\nRemap a value from one range to another. Float-only.\n\n```zig\nlo.remap(f64, 5.0, 0.0, 10.0, 0.0, 100.0); // 50.0\n```\n\n### clamp\n\nClamp a value to the range [lo, hi].\n\n```zig\nlo.clamp(i32, 15, 0, 10); // 10\nlo.clamp(i32, -5, 0, 10); // 0\nlo.clamp(i32, 5, 0, 10);  // 5\n```\n\n### inRange\n\nCheck if a value falls within the half-open range [start, end). Returns false if start \u003e= end.\n\n```zig\nlo.inRange(i32, 3, 1, 5); // true\nlo.inRange(i32, 5, 1, 5); // false (end is exclusive)\n```\n\n### cumSum\n\nCumulative sum. Caller owns the returned slice.\n\n```zig\nconst result = try lo.cumSum(i32, allocator, \u0026.{ 1, 2, 3, 4 });\ndefer allocator.free(result);\n// result == { 1, 3, 6, 10 }\n```\n\n### cumProd\n\nCumulative product. Caller owns the returned slice.\n\n```zig\nconst result = try lo.cumProd(i32, allocator, \u0026.{ 1, 2, 3, 4 });\ndefer allocator.free(result);\n// result == { 1, 2, 6, 24 }\n```\n\n### rangeAlloc\n\nAllocate a slice containing integers in [start, end). Caller owns the returned slice.\n\n```zig\nconst r = try lo.rangeAlloc(i32, allocator, 0, 5);\ndefer allocator.free(r);\n// r == .{ 0, 1, 2, 3, 4 }\n```\n\n### rangeWithStepAlloc\n\nAllocate a range with a custom step. Returns `error.InvalidArgument` for step 0. Caller owns the returned slice.\n\n```zig\nconst r = try lo.rangeWithStepAlloc(i32, allocator, 0, 10, 3);\ndefer allocator.free(r);\n// r == .{ 0, 3, 6, 9 }\n```\n\n---\n\n## Tuple Helpers\n\n### zip\n\nPair elements from two slices. Returns a lazy iterator. Stops at the shorter slice.\n\n```zig\nvar it = lo.zip(i32, u8, \u0026.{ 1, 2 }, \u0026.{ 'a', 'b' });\nit.next(); // .{ .a = 1, .b = 'a' }\n```\n\n### zipAlloc\n\nPair elements from two slices into an allocated slice. Caller owns the returned slice.\n\n```zig\nconst pairs = try lo.zipAlloc(i32, u8, allocator, \u0026.{ 1, 2 }, \u0026.{ 'a', 'b' });\ndefer allocator.free(pairs);\n```\n\n### zipWith\n\nZip two slices with a transform function. Returns a lazy iterator.\n\n```zig\nvar it = lo.zipWith(i32, i32, i32, \u0026.{ 1, 2 }, \u0026.{ 3, 4 }, addFn);\nit.next(); // 4\nit.next(); // 6\n```\n\n### unzip\n\nSplit a slice of pairs into two separate slices. Call `deinit(allocator)` to free.\n\n```zig\nconst r = try lo.unzip(i32, u8, allocator, \u0026pairs);\ndefer r.deinit(allocator);\n```\n\n### enumerate\n\nPair each element with its index. Returns a lazy iterator.\n\n```zig\nvar it = lo.enumerate(i32, \u0026.{ 10, 20, 30 });\nit.next(); // .{ .a = 0, .b = 10 }\nit.next(); // .{ .a = 1, .b = 20 }\n```\n\n---\n\n## Type Helpers\n\n### isNull\n\nReturns true if the optional value is null.\n\n```zig\nconst x: ?i32 = null;\nlo.isNull(i32, x); // true\n```\n\n### isNotNull\n\nReturns true if the optional value is non-null.\n\n```zig\nconst x: ?i32 = 42;\nlo.isNotNull(i32, x); // true\n```\n\n### unwrapOr\n\nUnwrap an optional, returning the fallback if null.\n\n```zig\nconst x: ?i32 = null;\nlo.unwrapOr(i32, x, 99); // 99\n```\n\n### coalesce\n\nReturns the first non-null value from a slice of optionals, or null if all are null.\n\n```zig\nconst vals = [_]?i32{ null, null, 42, 7 };\nlo.coalesce(i32, \u0026vals); // 42\n```\n\n### empty\n\nReturns the zero/default value for a type.\n\n```zig\nlo.empty(i32);  // 0\nlo.empty(bool); // false\n```\n\n### isEmpty\n\nReturns true if the value equals the zero/default for its type.\n\n```zig\nlo.isEmpty(i32, 0);   // true\nlo.isEmpty(i32, 42);  // false\n```\n\n### isNotEmpty\n\nReturns true if the value does not equal the zero/default for its type.\n\n```zig\nlo.isNotEmpty(i32, 42); // true\nlo.isNotEmpty(i32, 0);  // false\n```\n\n### ternary\n\nSelects one of two values based on a boolean condition. Both branches are evaluated eagerly.\n\n```zig\nlo.ternary(i32, true, 10, 20);  // 10\nlo.ternary(i32, false, 10, 20); // 20\n```\n\n### toConst\n\nConvert a mutable slice to a const slice.\n\n```zig\nvar buf = [_]i32{ 1, 2, 3 };\nconst view = lo.toConst(i32, \u0026buf);\n```\n\n---\n\n## Types\n\nThese are type constructors and result types used by the functions above.\n\n### Entry\n\nGeneric key-value pair for map utilities.\n\n```zig\nconst e = lo.Entry(u32, u8){ .key = 1, .value = 'a' };\n```\n\n### Pair\n\nGeneric pair type used by zip operations.\n\n```zig\nconst p = lo.Pair(i32, u8){ .a = 42, .b = 'z' };\n```\n\n### MinMax\n\nResult type returned by `minMax()`, holding `.min_val` and `.max_val`.\n\n### RangeError\n\nError set for `rangeWithStepAlloc`. Includes `InvalidArgument` for zero step.\n\n### PartitionResult\n\nResult type from `partition()` holding `.matching` and `.rest` slices. Call `deinit(allocator)` to free.\n\n### UnzipResult\n\nResult type from `unzip()` holding `.a` and `.b` slices. Call `deinit(allocator)` to free.\n\n### AssocEntry\n\nGeneric key-value entry type used by `associate`.\n\n```zig\nconst entry = lo.AssocEntry([]const u8, u32){ .key = \"alice\", .value = 30 };\n```\n\n### Iterator Types\n\nThe following iterator types are returned by their corresponding functions. They all implement a `next() -\u003e ?T` method, and most provide a `collect(allocator) -\u003e ![]T` method for eager evaluation.\n\n| Iterator | Returned by |\n|---|---|\n| `MapIterator` | `map()` |\n| `MapIndexIterator` | `mapIndex()` |\n| `FilterIterator` | `filter()` |\n| `RejectIterator` | `reject()` |\n| `FlattenIterator` | `flatten()` |\n| `FlatMapIterator` | `flatMap()` |\n| `CompactIterator` | `compact()` |\n| `ChunkIterator` | `chunk()` |\n| `FilterMapIterator` | `filterMapIter()` |\n| `WithoutIterator` | `without()` |\n| `ScanIterator` | `scan()` |\n| `WindowIterator` | `window()` |\n| `InterleaveIterator` | `interleave()` |\n| `TimesIterator` | `times()` |\n| `KeyIterator` | `keys()` |\n| `ValueIterator` | `values()` |\n| `EntryIterator` | `entries()` |\n| `ZipIterator` | `zip()` |\n| `ZipWithIterator` | `zipWith()` |\n| `EnumerateIterator` | `enumerate()` |\n| `WordIterator` | `words()` |\n| `StringChunkIterator` | `chunkString()` |\n\n## License\n\nSee [LICENSE](LICENSE).\n","funding_links":[],"categories":["Language Essentials"],"sub_categories":["Data Structure and Algorithm"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FOrlovEvgeny%2Flo.zig","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FOrlovEvgeny%2Flo.zig","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FOrlovEvgeny%2Flo.zig/lists"}