lo.zig
lo.zig is a Lodash-style Zig library
Generic utility library for Zig
Zero hidden allocations: functions that need memory take an Allocator.
Iterator-first: most transformations return lazy iterators.
Installation
Add lo.zig as a dependency in your build.zig.zon:
zig fetch --save git+https://github.com/OrlovEvgeny/lo.zig
Then in your build.zig:
const lo_dep = b.dependency("lo", .{
.target = target,
.optimize = optimize,
});
exe.root_module.addImport("lo", lo_dep.module("lo"));
Quick Start
const lo = @import("lo");
const total = lo.sum(i32, &.{ 1, 2, 3, 4 }); // 10
const head = lo.first(i32, &.{ 10, 20, 30 }); // 10
const safe = lo.unwrapOr(i32, null, 42); // 42
Function Index
- Slice Helpers - first, last, nth, firstOr, lastOr, nthOr, initial, tail, drop, dropRight, dropWhile, dropWhileAlloc, dropRightWhile, take, takeRight, takeWhile, takeWhileAlloc, takeRightWhile, sample, samples
- Transform - map, mapAlloc, mapIndex, filter, filterAlloc, reject, rejectAlloc, compact, compactAlloc, flatten, flattenAlloc, flattenDeep, flatMap, flatMapAlloc, without, forEach, forEachIndex, compactMap, filterMapIter
- 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
- Sort & Order - sortBy, sortByAlloc, sortByField, sortByFieldAlloc, toSortedAlloc, isSorted, equal, reverse, shuffle
- Set Operations - uniq, uniqBy, intersect, union_, difference, symmetricDifference, findDuplicates, findUniques, elementsMatch, differenceWith, intersectWith, unionWith
- Partition & Group - partition, groupBy, chunk, window, scan, scanAlloc
- Combine - concat, splice, interleave, fill, fillRange, repeat, repeatBy, times, timesAlloc
- Search - find, findIndex, findLast, findLastIndex, indexOf, lastIndexOf, contains, containsBy, every, some, none, minIndex, maxIndex, binarySearch, lowerBound, upperBound, sortedIndex, sortedLastIndex
- 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
- 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
- Math - sum, mean, median, variance, stddev, sampleVariance, sampleStddev, percentile, lerp, remap, clamp, inRange, cumSum, cumProd, rangeAlloc, rangeWithStepAlloc
- Tuple Helpers - zip, zipAlloc, zipWith, unzip, enumerate
- Type Helpers - isNull, isNotNull, unwrapOr, coalesce, empty, isEmpty, isNotEmpty, ternary, toConst
- Types - Entry, Pair, MinMax, RangeError, PartitionResult, UnzipResult, AssocEntry, and iterator types
Slice Helpers
first
Returns the first element of a slice, or null if empty.
lo.first(i32, &.{ 10, 20, 30 }); // 10
last
Returns the last element of a slice, or null if empty.
lo.last(i32, &.{ 10, 20, 30 }); // 30
nth
Element at the given index. Negative indices count from the end. Returns null if out of bounds.
lo.nth(i32, &.{ 10, 20, 30 }, -1); // 30
firstOr
Returns the first element, or a default if the slice is empty.
lo.firstOr(i32, &.{ 10, 20, 30 }, 0); // 10
lo.firstOr(i32, &.{}, 42); // 42
lastOr
Returns the last element, or a default if the slice is empty.
lo.lastOr(i32, &.{ 10, 20, 30 }, 0); // 30
lo.lastOr(i32, &.{}, 42); // 42
nthOr
Element at the given index with a default. Negative indices count from the end.
lo.nthOr(i32, &.{ 10, 20, 30 }, 1, 0); // 20
lo.nthOr(i32, &.{ 10, 20, 30 }, -1, 0); // 30
lo.nthOr(i32, &.{ 10, 20, 30 }, 5, 99); // 99
initial
All elements except the last. Empty slice if input is empty.
lo.initial(i32, &.{ 1, 2, 3 }); // &.{ 1, 2 }
tail
All elements except the first. Empty slice if input is empty.
lo.tail(i32, &.{ 1, 2, 3 }); // &.{ 2, 3 }
drop
Remove the first n elements, returning the rest as a sub-slice.
lo.drop(i32, &.{ 1, 2, 3, 4, 5 }, 2); // &.{ 3, 4, 5 }
dropRight
Remove the last n elements, returning the rest as a sub-slice.
lo.dropRight(i32, &.{ 1, 2, 3, 4, 5 }, 2); // &.{ 1, 2, 3 }
dropWhile
Drop leading elements while the predicate returns true.
lo.dropWhile(i32, &.{ 1, 2, 3, 4 }, isLessThan3); // &.{ 3, 4 }
dropWhileAlloc
Drop leading elements while the predicate returns true. Allocates a copy. Caller owns the returned slice.
const result = try lo.dropWhileAlloc(i32, allocator, &.{ 1, 2, 3, 4 }, isLessThan3);
defer allocator.free(result);
// result == &.{ 3, 4 }
dropRightWhile
Drop trailing elements while the predicate returns true.
lo.dropRightWhile(i32, &.{ 1, 2, 3, 4 }, isGt2); // &.{ 1, 2 }
take
Take the first n elements as a sub-slice.
lo.take(i32, &.{ 1, 2, 3, 4, 5 }, 3); // &.{ 1, 2, 3 }
takeRight
Take the last n elements as a sub-slice.
lo.takeRight(i32, &.{ 1, 2, 3, 4, 5 }, 2); // &.{ 4, 5 }
takeWhile
Take leading elements while the predicate returns true.
lo.takeWhile(i32, &.{ 1, 2, 3, 4 }, isLessThan3); // &.{ 1, 2 }
takeWhileAlloc
Take leading elements while the predicate returns true. Allocates a copy. Caller owns the returned slice.
const result = try lo.takeWhileAlloc(i32, allocator, &.{ 1, 2, 3, 4 }, isLessThan3);
defer allocator.free(result);
// result == &.{ 1, 2 }
takeRightWhile
Take trailing elements while the predicate returns true.
lo.takeRightWhile(i32, &.{ 1, 2, 3, 4 }, isGt2); // &.{ 3, 4 }
sample
Random element from a slice. Null if empty.
var prng = std.Random.DefaultPrng.init(0);
lo.sample(i32, &.{ 1, 2, 3 }, prng.random()); // random element
samples
N random elements from a slice (with replacement). Caller owns the returned slice.
const s = try lo.samples(i32, allocator, &.{ 1, 2, 3 }, 5, rng);
defer allocator.free(s);
Transform
map
Transform each element. Returns a lazy iterator.
var it = lo.map(i32, i64, &.{ 1, 2, 3 }, double);
it.next(); // 2
mapAlloc
Transform each element and collect into an allocated slice. Caller owns the returned slice.
const result = try lo.mapAlloc(i32, i32, allocator, &.{ 1, 2, 3 }, double);
defer allocator.free(result);
mapIndex
Transform each element with its index. Returns a lazy iterator.
var it = lo.mapIndex(i32, i64, &.{ 10, 20 }, addIndex);
it.next(); // addIndex(10, 0)
filter
Keep elements matching the predicate. Returns a lazy iterator.
var it = lo.filter(i32, &.{ 1, 2, 3, 4 }, isEven);
it.next(); // 2
it.next(); // 4
filterAlloc
Keep elements matching the predicate, collected into an allocated slice. Caller owns the returned slice.
const result = try lo.filterAlloc(i32, allocator, &.{ 1, 2, 3, 4 }, isEven);
defer allocator.free(result);
reject
Remove elements matching the predicate. Returns a lazy iterator.
var it = lo.reject(i32, &.{ 1, 2, 3, 4 }, isEven);
it.next(); // 1
it.next(); // 3
rejectAlloc
Remove elements matching the predicate, collected into an allocated slice. Caller owns the returned slice.
const result = try lo.rejectAlloc(i32, allocator, &.{ 1, 2, 3, 4 }, isEven);
defer allocator.free(result);
compact
Remove zero/null/default values. Returns a lazy iterator.
var it = lo.compact(?i32, &.{ 1, null, 3, null });
it.next(); // 1
it.next(); // 3
compactAlloc
Remove zero/null/default values into an allocated slice. Caller owns the returned slice.
const result = try lo.compactAlloc(?i32, allocator, &.{ 1, null, 3 });
defer allocator.free(result);
flatten
Flatten a slice of slices into a single sequence. Returns a lazy iterator.
const data = [_][]const i32{ &.{ 1, 2 }, &.{ 3, 4 } };
var it = lo.flatten(i32, &data);
// yields 1, 2, 3, 4
flattenAlloc
Flatten a slice of slices into an allocated slice. Counts total elements first, then allocates once. Caller owns the returned slice.
const data = [_][]const i32{ &.{ 1, 2 }, &.{ 3, 4, 5 } };
const result = try lo.flattenAlloc(i32, allocator, &data);
defer allocator.free(result);
// result == &.{ 1, 2, 3, 4, 5 }
flattenDeep
Flatten two levels of nesting ([][][]T to []T). Caller owns the returned slice.
const inner = [_][]const i32{ &.{ 1, 2 }, &.{ 3 } };
const outer = [_][]const []const i32{ &inner };
const result = try lo.flattenDeep(i32, allocator, &outer);
defer allocator.free(result);
// result == &.{ 1, 2, 3 }
flatMap
Map each element to a slice, then flatten into a single sequence. Returns a lazy iterator.
var it = lo.flatMap(i32, u8, &.{ 1, 2 }, toDigits);
flatMapAlloc
Map then flatten, collected into an allocated slice. Caller owns the returned slice.
const result = try lo.flatMapAlloc(i32, u8, allocator, &.{ 1, 2 }, toDigits);
defer allocator.free(result);
without
Exclude specific values from a slice. Returns a lazy iterator.
var it = lo.without(i32, &.{ 1, 2, 3, 4 }, &.{ 2, 4 });
// yields 1, 3
forEach
Invoke a function on each element.
lo.forEach(i32, &.{ 1, 2, 3 }, printFn);
forEachIndex
Invoke a function on each element with its index.
lo.forEachIndex(i32, &.{ 10, 20 }, printWithIndex);
compactMap
Filter and map in a single pass. The transform returns ?R; non-null values are collected into an allocated slice. Caller owns the returned slice.
const toEvenDoubled = struct {
fn f(x: i32) ?i32 {
if (@mod(x, 2) == 0) return x * 2;
return null;
}
}.f;
const result = try lo.compactMap(i32, i32, allocator, &.{ 1, 2, 3, 4 }, toEvenDoubled);
defer allocator.free(result);
// result == &.{ 4, 8 }
filterMapIter
Filter and map in a single step. Returns a lazy iterator.
var it = lo.filterMapIter(i32, i32, &.{ 1, 2, 3, 4 }, toEvenDoubled);
it.next(); // 4
it.next(); // 8
it.next(); // null
Aggregate
reduce
Left fold with an accumulator.
lo.reduce(i32, i32, &.{ 1, 2, 3 }, addFn, 0); // 6
reduceRight
Right fold with an accumulator. Processes elements right to left.
lo.reduceRight(i32, i32, &.{ 1, 2, 3 }, subtractFn, 0);
sum
Sum all elements in a slice. Returns 0 for empty slices.
lo.sum(i32, &.{ 1, 2, 3, 4 }); // 10
sumBy
Sum elements after applying a transform function.
lo.sumBy(i32, i64, &.{ 1, 2, 3 }, double); // 12
product
Multiply all elements in a slice. Returns 1 for empty slices.
lo.product(i32, &.{ 2, 3, 4 }); // 24
productBy
Multiply elements after applying a transform function.
lo.productBy(i32, i64, &.{ 2, 3, 4 }, double); // 192
mean
Arithmetic mean of a slice. Returns null for empty slices.
lo.mean(i32, &.{ 2, 4, 6 }).?; // 4.0
meanBy
Arithmetic mean after applying a transform function.
const asF64 = struct { fn f(x: i32) f64 { return @floatFromInt(x); } }.f;
lo.meanBy(i32, &vals, asF64).?; // 20.0
min
Returns the minimum value in a slice, or null if empty.
lo.min(i32, &.{ 3, 1, 2 }); // 1
max
Returns the maximum value in a slice, or null if empty.
lo.max(i32, &.{ 3, 1, 2 }); // 3
minBy
Returns the minimum element according to a comparator.
lo.minBy(Point, &points, Point.compareByX); // point with smallest x
maxBy
Returns the maximum element according to a comparator.
lo.maxBy(Point, &points, Point.compareByX); // point with largest x
minMax
Returns both min and max in a single pass. Null if empty.
const mm = lo.minMax(i32, &.{ 5, 1, 9, 3 }).?;
// mm.min_val == 1, mm.max_val == 9
minMaxBy
Returns both min and max in a single pass according to a custom comparator. Null if empty.
const byX = struct { fn f(a: Point, b: Point) std.math.Order {
return std.math.order(a.x, b.x);
} }.f;
const mm = lo.minMaxBy(Point, &points, byX).?;
// mm.min_val and mm.max_val
count
Count elements satisfying the predicate.
lo.count(i32, &.{ 1, 2, 3, 4 }, isEven); // 2
countBy
Count elements by a key function. Returns a frequency map.
var m = try lo.countBy(i32, bool, allocator, &.{ 1, 2, 3, 4, 5 }, isEvenFn);
defer m.deinit();
m.get(true).?; // 2
m.get(false).?; // 3
countValues
Build a frequency map: value -> number of occurrences. Caller owns the returned map.
var freq = try lo.countValues(i32, allocator, &.{ 1, 2, 2, 3 });
defer freq.deinit();
freq.get(2).?; // 2
mode
Returns the most frequently occurring value. Smallest value wins on ties. Null for empty slices.
const m = try lo.mode(i32, allocator, &.{ 1, 2, 2, 3, 2 });
// m == 2
median
Returns the median of a numeric slice, or null if empty. Allocates a temporary copy for sorting.
const m = try lo.median(i32, allocator, &.{ 1, 2, 3, 4 });
// m == 2.5
variance
Population variance (N denominator). Returns null for empty slices.
lo.variance(i32, &.{ 2, 4, 4, 4, 5, 5, 7, 9 }); // 4.0
stddev
Standard deviation (sqrt of population variance). Returns null for empty slices.
lo.stddev(i32, &.{ 2, 4, 4, 4, 5, 5, 7, 9 }); // 2.0
sampleVariance
Sample variance with N-1 denominator (Bessel’s correction). Returns null for slices with fewer than 2 elements.
lo.sampleVariance(i32, &.{ 2, 4, 4, 4, 5, 5, 7, 9 }); // ~4.571
sampleStddev
Sample standard deviation (sqrt of sample variance). Returns null for slices with fewer than 2 elements.
lo.sampleStddev(i32, &.{ 2, 4, 4, 4, 5, 5, 7, 9 }); // ~2.138
percentile
Returns the nth percentile using linear interpolation. Null for empty slices or if p is outside [0, 100].
const p = try lo.percentile(i32, allocator, &.{ 1, 2, 3, 4, 5 }, 50.0);
// p == 3.0
Sort & Order
sortBy
Sort a slice in-place by a key extracted via a function. Stable sort.
var items = [_]i32{ 30, 10, 20 };
lo.sortBy(i32, i32, &items, struct {
fn f(x: i32) i32 { return x; }
}.f);
// items == { 10, 20, 30 }
sortByAlloc
Returns a sorted copy without mutating the original. Caller owns the returned slice.
const sorted = try lo.sortByAlloc(i32, i32, allocator, &.{ 30, 10, 20 }, struct {
fn f(x: i32) i32 { return x; }
}.f);
defer allocator.free(sorted);
// sorted == { 10, 20, 30 }
sortByField
Sort a slice of structs in-place by a named field. Stable sort.
const Person = struct { name: []const u8, age: u32 };
var items = [_]Person{ .{ .name = "bob", .age = 30 }, .{ .name = "alice", .age = 25 } };
lo.sortByField(Person, &items, .age);
// items[0].age == 25, items[1].age == 30
sortByFieldAlloc
Returns a sorted copy of the slice sorted by a named struct field. Caller owns the returned slice.
const sorted = try lo.sortByFieldAlloc(Person, allocator, &people, .age);
defer allocator.free(sorted);
toSortedAlloc
Returns a sorted copy in natural ascending order. Caller owns the returned slice.
const sorted = try lo.toSortedAlloc(i32, allocator, &.{ 3, 1, 2 });
defer allocator.free(sorted);
// sorted == { 1, 2, 3 }
isSorted
True if the slice is sorted according to the comparator.
lo.isSorted(i32, &.{ 1, 2, 3 }, compareAsc); // true
equal
Element-wise equality of two slices.
lo.equal(i32, &.{ 1, 2, 3 }, &.{ 1, 2, 3 }); // true
reverse
Reverse a slice in-place.
var data = [_]i32{ 1, 2, 3 };
lo.reverse(i32, &data);
// data == .{ 3, 2, 1 }
shuffle
Fisher-Yates shuffle in-place.
var data = [_]i32{ 1, 2, 3, 4, 5 };
lo.shuffle(i32, &data, prng.random());
Set Operations
uniq
Remove duplicate elements. Preserves first occurrence order.
const u = try lo.uniq(i32, allocator, &.{ 1, 2, 2, 3, 1 });
defer allocator.free(u);
// u == &.{ 1, 2, 3 }
uniqBy
Remove duplicates by a key function. Preserves first occurrence order.
const u = try lo.uniqBy(Person, u32, allocator, &people, Person.id);
defer allocator.free(u);
intersect
Elements present in both slices. Order follows the first slice.
const i = try lo.intersect(i32, allocator, &.{ 1, 2, 3 }, &.{ 2, 3, 4 });
defer allocator.free(i);
// i == &.{ 2, 3 }
union_
Unique elements from both slices combined.
const u = try lo.union_(i32, allocator, &.{ 1, 2, 3 }, &.{ 2, 3, 4 });
defer allocator.free(u);
// u == &.{ 1, 2, 3, 4 }
difference
Elements in the first slice but not in the second.
const d = try lo.difference(i32, allocator, &.{ 1, 2, 3 }, &.{ 2, 4 });
defer allocator.free(d);
// d == &.{ 1, 3 }
symmetricDifference
Elements in either slice but not in both.
const sd = try lo.symmetricDifference(i32, allocator, &.{ 1, 2, 3 }, &.{ 2, 3, 4 });
defer allocator.free(sd);
// sd == &.{ 1, 4 }
findDuplicates
Find elements appearing more than once. Preserves first-occurrence order.
const dups = try lo.findDuplicates(i32, allocator, &.{ 1, 2, 2, 3, 3, 3 });
defer allocator.free(dups);
// dups == &.{ 2, 3 }
findUniques
Find elements appearing exactly once.
const uniques = try lo.findUniques(i32, allocator, &.{ 1, 2, 2, 3, 3, 3, 4 });
defer allocator.free(uniques);
// uniques == &.{ 1, 4 }
elementsMatch
True if two slices contain the same elements with the same multiplicities, regardless of order.
try lo.elementsMatch(i32, allocator, &.{ 1, 2, 3 }, &.{ 3, 2, 1 }); // true
try lo.elementsMatch(i32, allocator, &.{ 1, 1, 2 }, &.{ 1, 2, 2 }); // false
differenceWith
Elements in the first slice but not in the second, using a custom equality predicate.
const absEq = struct { fn f(a: i32, b: i32) bool { return @abs(a) == @abs(b); } }.f;
const d = try lo.differenceWith(i32, allocator, &.{ 1, 2, 3 }, &.{ -2, 4 }, absEq);
defer allocator.free(d);
// d == &.{ 1, 3 }
intersectWith
Elements present in both slices, using a custom equality predicate.
const i = try lo.intersectWith(i32, allocator, &.{ 1, -2, 3 }, &.{ 2, 4 }, absEq);
defer allocator.free(i);
// i == &.{ -2 }
unionWith
Unique elements from both slices combined, using a custom equality predicate.
const u = try lo.unionWith(i32, allocator, &.{ 1, 2 }, &.{ -2, 3 }, absEq);
defer allocator.free(u);
// u == &.{ 1, 2, 3 }
Partition & Group
partition
Split a slice into two: elements matching the predicate and the rest.
const p = try lo.partition(i32, allocator, &.{ 1, 2, 3, 4 }, isEven);
defer p.deinit(allocator);
// p.matching == &.{ 2, 4 }, p.rest == &.{ 1, 3 }
groupBy
Group elements by a key function. Caller owns the returned map.
var groups = try lo.groupBy(i32, bool, allocator, &.{ 1, 2, 3, 4 }, isEvenFn);
defer {
var it = groups.valueIterator();
while (it.next()) |list| list.deinit(allocator);
groups.deinit();
}
chunk
Split a slice into chunks of the given size. The last chunk may be smaller. Returns a lazy iterator.
var it = lo.chunk(i32, &.{ 1, 2, 3, 4, 5 }, 2);
it.next(); // &.{ 1, 2 }
it.next(); // &.{ 3, 4 }
it.next(); // &.{ 5 }
window
Sliding window over a slice. Returns a lazy iterator. Windows borrow from the input (zero allocation).
var it = lo.window(i32, &.{ 1, 2, 3, 4, 5 }, 3);
it.next(); // &.{ 1, 2, 3 }
it.next(); // &.{ 2, 3, 4 }
it.next(); // &.{ 3, 4, 5 }
it.next(); // null
scan
Like reduce but emits every intermediate result. Returns a lazy iterator.
const add = struct { fn f(a: i32, b: i32) i32 { return a + b; } }.f;
var it = lo.scan(i32, i32, &.{ 1, 2, 3 }, add, 0);
it.next(); // 1
it.next(); // 3
it.next(); // 6
scanAlloc
Eagerly compute all intermediate accumulator values into an allocated slice. Caller owns the returned slice.
const add = struct { fn f(a: i32, b: i32) i32 { return a + b; } }.f;
const result = try lo.scanAlloc(i32, i32, allocator, &.{ 1, 2, 3 }, add, 0);
defer allocator.free(result);
// result == &.{ 1, 3, 6 }
Combine
concat
Concatenate multiple slices into a single allocated slice. Caller owns the returned slice.
const result = try lo.concat(i32, allocator, &.{ &.{ 1, 2 }, &.{ 3, 4 }, &.{5} });
defer allocator.free(result);
// result == { 1, 2, 3, 4, 5 }
splice
Insert elements into a slice at a given index, returning a new allocated slice. Caller owns the returned slice.
const result = try lo.splice(i32, allocator, &.{ 1, 2, 5, 6 }, 2, &.{ 3, 4 });
defer allocator.free(result);
// result == { 1, 2, 3, 4, 5, 6 }
interleave
Round-robin interleave multiple slices. Returns a lazy iterator.
var it = lo.interleave(i32, &.{ &.{ 1, 2, 3 }, &.{ 4, 5, 6 } });
it.next(); // 1
it.next(); // 4
it.next(); // 2
it.next(); // 5
fill
Fill all elements with the given value (in-place).
var data = [_]i32{ 0, 0, 0 };
lo.fill(i32, &data, 42);
// data == .{ 42, 42, 42 }
fillRange
Fill elements in the range [start, end) with the given value (in-place).
var data = [_]i32{ 1, 2, 3, 4, 5 };
lo.fillRange(i32, &data, 0, 1, 4);
// data == .{ 1, 0, 0, 0, 5 }
repeat
Create a slice of n copies of a value. Caller owns the returned slice.
const r = try lo.repeat(i32, allocator, 42, 3);
defer allocator.free(r);
// r == &.{ 42, 42, 42 }
repeatBy
Create a slice of n elements produced by a callback. Caller owns the returned slice.
const r = try lo.repeatBy(i32, allocator, 3, indexSquared);
defer allocator.free(r);
// r == &.{ 0, 1, 4 }
times
Create a lazy iterator that calls a function N times with indices 0..N-1.
var iter = lo.times(usize, 4, square);
while (iter.next()) |val| { ... }
timesAlloc
Eagerly call a function N times and return the results. Caller owns the returned slice.
const squares = try lo.timesAlloc(usize, allocator, 4, square);
defer allocator.free(squares);
// squares == { 0, 1, 4, 9 }
Search
find
First element matching the predicate, or null.
lo.find(i32, &.{ 1, 2, 3, 4 }, isEven); // 2
findIndex
Index of the first element matching the predicate, or null.
lo.findIndex(i32, &.{ 1, 2, 3 }, isEven); // 1
findLast
Last element matching the predicate, or null.
lo.findLast(i32, &.{ 1, 2, 3, 4 }, isEven); // 4
findLastIndex
Index of the last element matching the predicate, or null.
lo.findLastIndex(i32, &.{ 1, 2, 3, 4 }, isEven); // 3
indexOf
Index of the first occurrence of a value, or null.
lo.indexOf(i32, &.{ 10, 20, 30 }, 20); // 1
lastIndexOf
Index of the last occurrence of a value, or null.
lo.lastIndexOf(i32, &.{ 1, 2, 3, 2 }, 2); // 3
contains
True if the slice contains the given value.
lo.contains(i32, &.{ 1, 2, 3 }, 2); // true
containsBy
True if any element satisfies the predicate.
lo.containsBy(i32, &.{ 1, 2, 3 }, isEven); // true
every
True if all elements satisfy the predicate. True for empty slices.
lo.every(i32, &.{ 2, 4, 6 }, isEven); // true
some
True if at least one element satisfies the predicate. False for empty slices.
lo.some(i32, &.{ 1, 2, 3 }, isEven); // true
none
True if no elements satisfy the predicate. True for empty slices.
lo.none(i32, &.{ 1, 3, 5 }, isEven); // true
minIndex
Returns the index of the minimum element, or null if empty. First occurrence on ties.
lo.minIndex(i32, &.{ 3, 1, 4, 1, 5 }); // 1
maxIndex
Returns the index of the maximum element, or null if empty. First occurrence on ties.
lo.maxIndex(i32, &.{ 3, 1, 4, 1, 5 }); // 4
binarySearch
Binary search for a target in a sorted ascending slice. Returns the index or null. O(log n).
lo.binarySearch(i32, &.{ 1, 3, 5, 7, 9 }, 5); // Some(2)
lo.binarySearch(i32, &.{ 1, 3, 5, 7, 9 }, 4); // null
lowerBound
Index of the first element >= target in a sorted slice. Returns slice.len if all are less.
lo.lowerBound(i32, &.{ 1, 3, 5, 7 }, 4); // 2
lo.lowerBound(i32, &.{ 1, 3, 5, 7 }, 5); // 2
lo.lowerBound(i32, &.{ 1, 3, 5, 7 }, 9); // 4
upperBound
Index of the first element > target in a sorted slice. Returns slice.len if all are <= target.
lo.upperBound(i32, &.{ 1, 3, 5, 7 }, 3); // 2
lo.upperBound(i32, &.{ 1, 3, 5, 7 }, 4); // 2
lo.upperBound(i32, &.{ 1, 3, 5, 7 }, 9); // 4
sortedIndex
Insertion index to maintain sorted order. Equivalent to lowerBound.
lo.sortedIndex(i32, &.{ 1, 3, 5, 7 }, 4); // 2
sortedLastIndex
Insertion index after the last occurrence of a value. Equivalent to upperBound.
lo.sortedLastIndex(i32, &.{ 1, 3, 3, 5 }, 3); // 3
Map Helpers
keys
Iterate over map keys. Returns a lazy iterator.
var it = lo.keys(u32, u8, &my_map);
while (it.next()) |key| { ... }
keysAlloc
Collect all keys into an allocated slice. Caller owns the returned slice.
const ks = try lo.keysAlloc(u32, u8, allocator, &my_map);
defer allocator.free(ks);
values
Iterate over map values. Returns a lazy iterator.
var it = lo.values(u32, u8, &my_map);
while (it.next()) |val| { ... }
valuesAlloc
Collect all values into an allocated slice. Caller owns the returned slice.
const vs = try lo.valuesAlloc(u32, u8, allocator, &my_map);
defer allocator.free(vs);
entries
Iterate over key-value pairs. Returns a lazy iterator.
var it = lo.entries(u32, u8, &my_map);
while (it.next()) |e| { _ = e.key; _ = e.value; }
entriesAlloc
Collect all key-value pairs into an allocated slice. Caller owns the returned slice.
const es = try lo.entriesAlloc(u32, u8, allocator, &my_map);
defer allocator.free(es);
fromEntries
Build a map from a slice of key-value pairs. Caller owns the returned map.
const pairs = [_]lo.Entry(u32, u8){ .{ .key = 1, .value = 'a' } };
var m = try lo.fromEntries(u32, u8, allocator, &pairs);
defer m.deinit();
mapKeys
Transform map keys using a function. Caller owns the returned map.
var result = try lo.mapKeys(u32, u8, u64, allocator, &m, timesTwo);
defer result.deinit();
mapValues
Transform map values using a function. Caller owns the returned map.
var result = try lo.mapValues(u32, u8, u16, allocator, &m, multiply);
defer result.deinit();
filterMap
Filter map entries by a predicate on key and value. Caller owns the returned map.
var result = try lo.filterMap(u32, u8, allocator, &m, keyGt1);
defer result.deinit();
filterKeys
Filter map entries by a predicate on the key. Caller owns the returned map.
var result = try lo.filterKeys(u32, u8, allocator, &m, isEven);
defer result.deinit();
filterValues
Filter map entries by a predicate on the value. Caller owns the returned map.
var result = try lo.filterValues(u32, u8, allocator, &m, isPositive);
defer result.deinit();
pickKeys
Keep only entries with the specified keys. Caller owns the returned map.
var result = try lo.pickKeys(u32, u8, allocator, &m, &.{ 1, 3 });
defer result.deinit();
omitKeys
Remove entries with the specified keys. Caller owns the returned map.
var result = try lo.omitKeys(u32, u8, allocator, &m, &.{ 2, 3 });
defer result.deinit();
invert
Swap keys and values. Caller owns the returned map.
var result = try lo.invert(u32, u8, allocator, &m);
defer result.deinit();
merge
Merge entries from source into dest. Source values overwrite on conflict.
try lo.merge(u32, u8, &dest, &source);
assign
Merge N maps into one with last-write-wins semantics. Caller owns the returned map.
var result = try lo.assign(u32, u8, allocator, &.{ &m1, &m2 });
defer result.deinit();
mapEntries
Transform both keys and values of a map. Caller owns the returned map.
var result = try lo.mapEntries(u32, u8, u64, u16, allocator, &m, xform);
defer result.deinit();
mapToSlice
Transform map entries into an allocated slice. Caller owns the returned slice.
const result = try lo.mapToSlice(u32, u8, u64, allocator, &m, sumKeyVal);
defer allocator.free(result);
valueOr
Get a value from the map, or return a default if the key is absent.
lo.valueOr(u32, u8, &my_map, 999, 0); // 0 if 999 not in map
hasKey
True if the map contains the given key.
lo.hasKey(u32, u8, &m, 1); // true
mapCount
Number of entries in the map.
lo.mapCount(u32, u8, &m); // 3
keyBy
Convert a slice to a map indexed by an extracted key. Last element wins on duplicate keys.
var m = try lo.keyBy(Person, u32, allocator, &people, getAge);
defer m.deinit();
associate
Convert a slice to a map with custom key and value extraction. Last element wins on duplicate keys.
var m = try lo.associate(Person, u32, []const u8, allocator, &people, toEntry);
defer m.deinit();
String Helpers
words
Split a string into words at camelCase, PascalCase, snake_case, kebab-case, and whitespace boundaries. Returns a lazy iterator.
var it = lo.words("helloWorld");
it.next(); // "hello"
it.next(); // "World"
wordsAlloc
Split a string into words, collected into an allocated slice. Caller owns the returned slice.
const ws = try lo.wordsAlloc(allocator, "camelCase");
defer allocator.free(ws);
// ws: &.{ "camel", "Case" }
camelCase
Convert a string to camelCase. Caller owns the returned string.
const s = try lo.camelCase(allocator, "hello_world");
defer allocator.free(s);
// s == "helloWorld"
pascalCase
Convert a string to PascalCase. Caller owns the returned string.
const s = try lo.pascalCase(allocator, "hello_world");
defer allocator.free(s);
// s == "HelloWorld"
snakeCase
Convert a string to snake_case. Caller owns the returned string.
const s = try lo.snakeCase(allocator, "helloWorld");
defer allocator.free(s);
// s == "hello_world"
kebabCase
Convert a string to kebab-case. Caller owns the returned string.
const s = try lo.kebabCase(allocator, "helloWorld");
defer allocator.free(s);
// s == "hello-world"
capitalize
Capitalize the first letter of a string. Caller owns the returned string.
const s = try lo.capitalize(allocator, "hello");
defer allocator.free(s);
// s == "Hello"
lowerFirst
Lowercase just the first character (ASCII). Caller owns the returned string.
const s = try lo.lowerFirst(allocator, "Hello");
defer allocator.free(s);
// s == "hello"
toLower
Convert an entire string to lowercase (ASCII). Caller owns the returned string.
const s = try lo.toLower(allocator, "Hello World");
defer allocator.free(s);
// s == "hello world"
toUpper
Convert an entire string to uppercase (ASCII). Caller owns the returned string.
const s = try lo.toUpper(allocator, "Hello World");
defer allocator.free(s);
// s == "HELLO WORLD"
trim
Trim whitespace from both ends of a string. Returns a sub-slice (zero-copy).
lo.trim(" hello "); // "hello"
trimStart
Trim whitespace from the start of a string. Returns a sub-slice (zero-copy).
lo.trimStart(" hello "); // "hello "
trimEnd
Trim whitespace from the end of a string. Returns a sub-slice (zero-copy).
lo.trimEnd(" hello "); // " hello"
startsWith
Check if a string starts with a given prefix.
lo.startsWith("hello world", "hello"); // true
endsWith
Check if a string ends with a given suffix.
lo.endsWith("hello world", "world"); // true
includes
Check if a string contains a substring.
lo.includes("hello world", "world"); // true
substr
Extract a substring by start and end byte indices. Indices are clamped. Returns a sub-slice (zero-copy).
lo.substr("hello", 2, 5); // "llo"
ellipsis
Truncate a string and add “…” if it exceeds max_len. Caller owns the returned string.
const s = try lo.ellipsis(allocator, "hello world", 8);
defer allocator.free(s);
// s == "hello..."
strRepeat
Repeat a string n times. Caller owns the returned string.
const s = try lo.strRepeat(allocator, "ab", 3);
defer allocator.free(s);
// s == "ababab"
padLeft
Left-pad a string to the given length. Caller owns the returned string.
const s = try lo.padLeft(allocator, "42", 5, '0');
defer allocator.free(s);
// s == "00042"
padRight
Right-pad a string to the given length. Caller owns the returned string.
const s = try lo.padRight(allocator, "hi", 5, '.');
defer allocator.free(s);
// s == "hi..."
runeLength
Count the number of Unicode codepoints in a UTF-8 string. Returns error.InvalidUtf8 for invalid input.
try lo.runeLength("hello"); // 5
try lo.runeLength("\xc3\xa9"); // 1
randomString
Generate a random alphanumeric string. Caller owns the returned string.
const s = try lo.randomString(allocator, 10, prng.random());
defer allocator.free(s);
split
Split a string by a delimiter sequence. Returns a lazy iterator. Preserves empty tokens.
var it = lo.split("one,two,,four", ",");
it.next(); // "one"
it.next(); // "two"
it.next(); // ""
it.next(); // "four"
splitAlloc
Split a string by a delimiter, collected into an allocated slice. Caller owns the returned outer slice.
const parts = try lo.splitAlloc(allocator, "a-b-c", "-");
defer allocator.free(parts);
// parts[0] == "a", parts[1] == "b", parts[2] == "c"
join
Join a slice of strings with a separator. Caller owns the returned string.
const s = try lo.join(allocator, ", ", &.{ "hello", "world" });
defer allocator.free(s);
// s == "hello, world"
replace
Replace the first occurrence of a needle. Caller owns the returned string.
const s = try lo.replace(allocator, "hello hello", "hello", "hi");
defer allocator.free(s);
// s == "hi hello"
replaceAll
Replace all occurrences of a needle. Caller owns the returned string.
const s = try lo.replaceAll(allocator, "hello hello", "hello", "hi");
defer allocator.free(s);
// s == "hi hi"
chunkString
Split a string into fixed-size byte chunks. Returns a lazy iterator. The last chunk may be smaller.
var it = lo.chunkString("abcdefgh", 3);
it.next(); // "abc"
it.next(); // "def"
it.next(); // "gh"
Math
sum
Sum all elements. Returns 0 for empty slices.
lo.sum(i32, &.{ 1, 2, 3, 4 }); // 10
mean
Arithmetic mean. Returns null for empty slices.
lo.mean(i32, &.{ 2, 4, 6 }).?; // 4.0
median
Median value. Allocates a temporary sorted copy. Null for empty slices.
const m = try lo.median(i32, allocator, &.{ 1, 2, 3, 4 });
// m == 2.5
variance
Population variance (N denominator). Null for empty slices.
lo.variance(i32, &.{ 2, 4, 4, 4, 5, 5, 7, 9 }); // 4.0
stddev
Standard deviation (sqrt of population variance). Null for empty slices.
lo.stddev(i32, &.{ 2, 4, 4, 4, 5, 5, 7, 9 }); // 2.0
sampleVariance
Sample variance with N-1 denominator (Bessel’s correction). Null for slices with fewer than 2 elements.
lo.sampleVariance(i32, &.{ 2, 4, 4, 4, 5, 5, 7, 9 }); // ~4.571
sampleStddev
Sample standard deviation (sqrt of sample variance). Null for slices with fewer than 2 elements.
lo.sampleStddev(i32, &.{ 2, 4, 4, 4, 5, 5, 7, 9 }); // ~2.138
percentile
Nth percentile using linear interpolation. Null for empty slices or p outside [0, 100].
const p = try lo.percentile(i32, allocator, &.{ 1, 2, 3, 4, 5 }, 50.0);
// p == 3.0
lerp
Linear interpolation between two values. Float-only.
lo.lerp(f64, 0.0, 10.0, 0.5); // 5.0
remap
Remap a value from one range to another. Float-only.
lo.remap(f64, 5.0, 0.0, 10.0, 0.0, 100.0); // 50.0
clamp
Clamp a value to the range [lo, hi].
lo.clamp(i32, 15, 0, 10); // 10
lo.clamp(i32, -5, 0, 10); // 0
lo.clamp(i32, 5, 0, 10); // 5
inRange
Check if a value falls within the half-open range [start, end). Returns false if start >= end.
lo.inRange(i32, 3, 1, 5); // true
lo.inRange(i32, 5, 1, 5); // false (end is exclusive)
cumSum
Cumulative sum. Caller owns the returned slice.
const result = try lo.cumSum(i32, allocator, &.{ 1, 2, 3, 4 });
defer allocator.free(result);
// result == { 1, 3, 6, 10 }
cumProd
Cumulative product. Caller owns the returned slice.
const result = try lo.cumProd(i32, allocator, &.{ 1, 2, 3, 4 });
defer allocator.free(result);
// result == { 1, 2, 6, 24 }
rangeAlloc
Allocate a slice containing integers in [start, end). Caller owns the returned slice.
const r = try lo.rangeAlloc(i32, allocator, 0, 5);
defer allocator.free(r);
// r == .{ 0, 1, 2, 3, 4 }
rangeWithStepAlloc
Allocate a range with a custom step. Returns error.InvalidArgument for step 0. Caller owns the returned slice.
const r = try lo.rangeWithStepAlloc(i32, allocator, 0, 10, 3);
defer allocator.free(r);
// r == .{ 0, 3, 6, 9 }
Tuple Helpers
zip
Pair elements from two slices. Returns a lazy iterator. Stops at the shorter slice.
var it = lo.zip(i32, u8, &.{ 1, 2 }, &.{ 'a', 'b' });
it.next(); // .{ .a = 1, .b = 'a' }
zipAlloc
Pair elements from two slices into an allocated slice. Caller owns the returned slice.
const pairs = try lo.zipAlloc(i32, u8, allocator, &.{ 1, 2 }, &.{ 'a', 'b' });
defer allocator.free(pairs);
zipWith
Zip two slices with a transform function. Returns a lazy iterator.
var it = lo.zipWith(i32, i32, i32, &.{ 1, 2 }, &.{ 3, 4 }, addFn);
it.next(); // 4
it.next(); // 6
unzip
Split a slice of pairs into two separate slices. Call deinit(allocator) to free.
const r = try lo.unzip(i32, u8, allocator, &pairs);
defer r.deinit(allocator);
enumerate
Pair each element with its index. Returns a lazy iterator.
var it = lo.enumerate(i32, &.{ 10, 20, 30 });
it.next(); // .{ .a = 0, .b = 10 }
it.next(); // .{ .a = 1, .b = 20 }
Type Helpers
isNull
Returns true if the optional value is null.
const x: ?i32 = null;
lo.isNull(i32, x); // true
isNotNull
Returns true if the optional value is non-null.
const x: ?i32 = 42;
lo.isNotNull(i32, x); // true
unwrapOr
Unwrap an optional, returning the fallback if null.
const x: ?i32 = null;
lo.unwrapOr(i32, x, 99); // 99
coalesce
Returns the first non-null value from a slice of optionals, or null if all are null.
const vals = [_]?i32{ null, null, 42, 7 };
lo.coalesce(i32, &vals); // 42
empty
Returns the zero/default value for a type.
lo.empty(i32); // 0
lo.empty(bool); // false
isEmpty
Returns true if the value equals the zero/default for its type.
lo.isEmpty(i32, 0); // true
lo.isEmpty(i32, 42); // false
isNotEmpty
Returns true if the value does not equal the zero/default for its type.
lo.isNotEmpty(i32, 42); // true
lo.isNotEmpty(i32, 0); // false
ternary
Selects one of two values based on a boolean condition. Both branches are evaluated eagerly.
lo.ternary(i32, true, 10, 20); // 10
lo.ternary(i32, false, 10, 20); // 20
toConst
Convert a mutable slice to a const slice.
var buf = [_]i32{ 1, 2, 3 };
const view = lo.toConst(i32, &buf);
Types
These are type constructors and result types used by the functions above.
Entry
Generic key-value pair for map utilities.
const e = lo.Entry(u32, u8){ .key = 1, .value = 'a' };
Pair
Generic pair type used by zip operations.
const p = lo.Pair(i32, u8){ .a = 42, .b = 'z' };
MinMax
Result type returned by minMax(), holding .min_val and .max_val.
RangeError
Error set for rangeWithStepAlloc. Includes InvalidArgument for zero step.
PartitionResult
Result type from partition() holding .matching and .rest slices. Call deinit(allocator) to free.
UnzipResult
Result type from unzip() holding .a and .b slices. Call deinit(allocator) to free.
AssocEntry
Generic key-value entry type used by associate.
const entry = lo.AssocEntry([]const u8, u32){ .key = "alice", .value = 30 };
Iterator Types
The following iterator types are returned by their corresponding functions. They all implement a next() -> ?T method, and most provide a collect(allocator) -> ![]T method for eager evaluation.
| Iterator | Returned by |
|---|---|
MapIterator |
map() |
MapIndexIterator |
mapIndex() |
FilterIterator |
filter() |
RejectIterator |
reject() |
FlattenIterator |
flatten() |
FlatMapIterator |
flatMap() |
CompactIterator |
compact() |
ChunkIterator |
chunk() |
FilterMapIterator |
filterMapIter() |
WithoutIterator |
without() |
ScanIterator |
scan() |
WindowIterator |
window() |
InterleaveIterator |
interleave() |
TimesIterator |
times() |
KeyIterator |
keys() |
ValueIterator |
values() |
EntryIterator |
entries() |
ZipIterator |
zip() |
ZipWithIterator |
zipWith() |
EnumerateIterator |
enumerate() |
WordIterator |
words() |
StringChunkIterator |
chunkString() |
License
See LICENSE.