Lists

September 3, 2026 · View on GitHub

 ███████╗████████╗██████╗ ██╗   ██╗██╗  ██╗███████╗
 ██╔════╝╚══██╔══╝██╔══██╗╚██╗ ██╔╝██║ ██╔╝██╔════╝
 ███████╗   ██║   ██████╔╝ ╚████╔╝ █████╔╝ █████╗
 ╚════██║   ██║   ██╔══██╗  ╚██╔╝  ██╔═██╗ ██╔══╝
 ███████║   ██║   ██║  ██║   ██║   ██║  ██╗███████╗
 ╚══════╝   ╚═╝   ╚═╝  ╚═╝   ╚═╝   ╚═╝  ╚═╝╚══════╝
                   [ u t i l s ]

CI License: MIT stryke

[BOUNDARY HELPERS // EVERYTHING ELSE IS A STRYKE BUILTIN]

"149 composites the language doesn't already ship. Cross-checked against %b — zero overlap."

stryke-utils is a small pure-stryke utility library: six sublibraries (String, List, Hash, Num, Time, Path) of higher-level composites that aren't already in stryke core. No [ffi] table, no cdylib, no helper binary — just .stk modules loaded on use Utils. Created by MenkeTechnologies.

strykelang · MenkeTechnologiesMeta · stryke-arrow · stryke-demo

Read the Docs · Engineering Report


Table of Contents


[0x00] Why a Package, Not Core

stryke core absorbed most of the obvious composites — slugify, snake_case, truncate, levenshtein, chunk, uniq, group_by, deep_merge, pick, omit, clamp, format_bytes, format_duration, now_ms, basename, common_prefix, and roughly seventy more landed in %b. This package shrunk in response. What's left here is the long tail: helpers that are still useful glue but live below the bar for core inclusion.

TierPropertiesExamples
stryke core (%b, 10k+ entries)builtins everyone needs everywherelength, keys, uc, sort, time, sprintf, slugify, chunk, deep_merge, format_bytes, levenshtein, basename, …
stryke-utils (opt-in, 149 fns)path-aware variants, n-ary wrappers, regex composites, the long taildeep_merge_all, parse_duration, compound_ext, round_to_multiple, pad_center, escape_shell, mask_middle, unwrap, windows, difference/intersection/union

Every function in this repo is cross-checked against %b at build time — zero name collisions with builtins. The two exceptions, Utils::Path::join and Utils::Path::normalize, are intentionally distinct: stryke's join is the array builtin and stryke's normalize is a vector op.

[0x01] Install

From a release:

s pkg install -g github.com/MenkeTechnologies/stryke-utils

From a local checkout:

cd ~/projects/stryke-utils
s pkg install -g .              # installs into ~/.stryke/store/stryke-utils@<version>/

Or:

make install

No cargo step. No cdylib. The store directory ends up containing stryke.toml + lib/*.stk only.

[0x02] Quick Start

use Utils                                       # pulls all six sublibraries

# Strings — composites that aren't in core
Utils::String::ltrim("   hello")                # "hello"
Utils::String::pad_center("hi", 8, ".")         # "...hi..."
Utils::String::squeeze("aaabbc")                # "abc"
Utils::String::compact_whitespace("a   b\t c")  # "a b c"
Utils::String::partition("a/b/c", "/")          # ("a", "/", "b/c")
Utils::String::rpartition("a/b/c", "/")         # ("a/b", "/", "c")
Utils::String::mask_middle("4111232233334444", 4, 4)  # "4111********4444"
Utils::String::escape_shell("it's \$foo")       # "'it'\\''s \$foo'"
Utils::String::unwrap('"quoted"', '"')          # "quoted"

# Lists — set ops + sliding windows
Utils::List::difference([1,2,3,4], [2,4])       # [1,3]
Utils::List::intersection([1,2,3], [2,3,5])     # [2,3]
Utils::List::union([1,2], [2,3])                # [1,2,3]
Utils::List::windows([1,2,3,4], 2)              # [[1,2],[2,3],[3,4]]
Utils::List::transpose([[1,2,3],[4,5,6]])       # [[1,4],[2,5],[3,6]]

# Hashes — variadic merge + dot-path access
val $cfg = Utils::Hash::deep_merge_all(
    $defaults, $from_env, $runtime)
Utils::Hash::deep_get($cfg, "db.pool.max")      # nested read by dot path
Utils::Hash::deep_set($cfg, "db.pool.min", 5)   # autovivifying write
Utils::Hash::deep_has($cfg, "db.pool.min")      # missing-vs-undef differentiator

# Numbers — long tail
Utils::Num::ordinal(21)                         # "21st"
Utils::Num::round_to_multiple(13, 5)            # 15

# Time — parsing + relative phrasing + ISO formatting
Utils::Time::parse_duration("1h30m")            # 5400
Utils::Time::ago(time() - 90)                   # "1 minute ago"
Utils::Time::format_iso8601()                   # "2026-06-10T14:23:05Z"

# Paths — string-only path arithmetic
Utils::Path::compound_ext("archive.tar.gz")     # "tar.gz"
Utils::Path::set_ext("a/b.csv", "parquet")      # "a/b.parquet"
Utils::Path::normalize("/a/b/../c/./d")         # "/a/c/d"
Utils::Path::relative("/a/x", "/a/b/c")         # "../../x"
Utils::Path::join("/a", "b/", "/c")             # "/a/b/c"

For everything else — slugify, chunk, uniq, deep_merge, format_bytes, levenshtein, basename, … — just call the stryke builtin directly. No wrapper indirection.

Pull only what you need:

use Utils::Path
use Utils::Time

Utils::Path::compound_ext("a.tar.gz")
Utils::Time::parse_duration("90m")

[0x03] Sublibraries

ModuleusefnsSurface
Stringuse Utils::String33ltrim · rtrim · pad_center · visible_width · count_occurrences · reverse_chars · squeeze · compact_whitespace · partition · rpartition · mask_middle · escape_shell · expand_tabs · unexpand_tabs · unwrap · ellipsize · truncate_words · nth_index · splitn · capitalize_first · uncapitalize · titleize · normalize_newlines · collapse_blank_lines · strip_quotes · repeat_to · remove_prefix · remove_suffix · ensure_prefix · ensure_suffix · prefix_lines · nth_line · count_lines_nonblank
Listuse Utils::List20difference · intersection · union · symmetric_difference · is_disjoint · windows · chunk · cartesian · rle_encode · rle_decode · top_n · bottom_n · count_where · transpose · running_sum · running_product · round_robin · deltas · rotate_left · index_of_max
Hashuse Utils::Hash26deep_merge_all · deep_get · deep_set · deep_has · map_keys · map_values · all_hashes · rename_keys · flatten_keys · unflatten_keys · deep_delete · defaults · deep_keys · deep_values · count_values · map_entries · merge_with · pick_by · omit_by · hash_diff · invert_multi · deep_count · pluck_paths · to_query_string (+ _vstr, _urlenc helpers)
Numuse Utils::Num26round_to_multiple · floor_to_multiple · ceil_to_multiple · ordinal · percent_change · weighted_avg · percentile_of · digit_sum · digit_count · digital_root · pct_of · mean_abs_dev · is_close · to_radians · to_degrees · round_sig · clamp01 · remap · wrap · gcd · round_half_even · clamp_to · mod_floor · next_power_of_two · midrange · divisors_of
Timeuse Utils::Time18parse_duration · ago · format_iso8601 · format_date · format_time · timed · day_of_week · format_human · format_clock · add_duration · sub_duration · parse_iso8601 · quarter · is_same_day · truncate_to_hour · next_weekday · prev_weekday (+ _days_from_civil helper)
Pathuse Utils::Path26ext · compound_ext · without_ext · set_ext · splitext · join · normalize · is_absolute · is_bare · relative · with_name · add_suffix · strip_trailing_slash · ensure_trailing_slash · segments · depth · is_under · is_hidden · expand_user · sibling · ancestors · with_stem · common_ancestor · is_relative · with_parent · strip_extension_all

149 functions total. Every sublibrary stands alone — no FFI, no required environment, no state between calls. Drop a single lib/*.stk into another project and it works.

[0x04] What's NOT in Here

By design — these are stryke builtins, so we don't re-wrap them. Every omission below was present in stryke-utils at one point and got deleted when the corresponding builtin landed in core:

CategoryBuiltins (call directly)
String case/slugtrim · slugify · snake_case · kebab_case · camel_case · pascal_case · title_case · swap_case · indent · dedent · rot13
String predicates / distancecontains · starts_with · ends_with · is_blank · is_palindrome · levenshtein · hamming · dice_coefficient · find_all_indices · common_prefix · common_suffix
String padding / shapingpad_left · pad_right · truncate · strip_ansi · word_wrap
Listchunk · compact · uniq · uniq_by · flatten · group_by · count_by · index_by · partition · pluck · sum · mean · median · min_by · max_by · sort_by · zip · take · drop · range
Hashdeep_merge · pick · omit · invert · filter · from_pairs · to_pairs · is_empty
Numclamp · between · lerp · round_to · format_number · format_bytes · format_percent · gcd · lcm · sign · is_even · is_odd
Timenow_ms · now_us · format_duration · elapsed
Pathbasename · dirname · common_prefix
Coreuc/lc/length/sprintf/substr/index/rindex/split · push/pop/shift/unshift/splice/sort/reverse/map/grep/join/keys/values/exists/defined/scalar/wantarray · abs/int/sqrt/time/localtime/gmtime/sleep · mkdir/unlink/rmdir/opendir/readdir/-e/-d/-f/-s · to_json/from_json · the .. range and x repetition operators

If a function here can be replaced with one builtin call, it's a bug — file an issue.

[0x05] CLI

bin/utils.stk is a thin dispatcher exposing the surviving lib fns plus convenience routes to a few common builtins:

s bin/utils.stk pad-center "hi" 8 .            # ...hi...
s bin/utils.stk squeeze "aaabbc"               # abc
s bin/utils.stk mask 4111232233334444 4 4      # 4111********4444
s bin/utils.stk escape-shell "it's \$foo"      # 'it'\''s $foo'
s bin/utils.stk unwrap '"quoted"' '"'          # quoted
s bin/utils.stk ordinal 21                     # 21st
s bin/utils.stk round-multiple 13 5            # 15
s bin/utils.stk parse-duration 1h30m           # 5400
s bin/utils.stk ago $((now - 90))              # 1 minute ago
s bin/utils.stk iso                            # 2026-06-10T14:23:05Z
s bin/utils.stk compound-ext archive.tar.gz    # tar.gz
s bin/utils.stk normalize a/./b/../c           # a/c
s bin/utils.stk relative /a/x /a/b/c           # ../../x
s bin/utils.stk help

For builtins (slugify, chunk, deep_merge, format_bytes, …), call stryke one-liner-style — no wrapper needed:

s -e 'print slugify($ARGV[0])' -- "Hello, World!"

[0x06] Tests

s test t/                       # assertions across every public function
make check-counts               # verify every doc count matches the source

t/test_utils.stk covers every public function with at least one round-trip / boundary assertion and exits TAP-style via test_run.

scripts/check-counts.sh derives the per-sublib fn counts, the total, the assertion count, and the example count straight from lib/, t/, and examples/, then fails if any number quoted in README.md or docs/*.html disagrees — so the hand-written doc numbers can't silently rot. It runs in CI on every push.

[0x07] Layout

stryke-utils/
  stryke.toml                  # pure-stryke package manifest (no [ffi])
  Makefile                     # test / check-counts / install / clean
  LICENSE                      # MIT
  scripts/
    check-counts.sh            # doc-count invariant (CI-enforced)
  lib/
    Utils.stk                  # `use Utils` — pulls all six sublibs
    String.stk                 # `use Utils::String`
    List.stk                   # `use Utils::List`
    Hash.stk                   # `use Utils::Hash`
    Num.stk                    # `use Utils::Num`
    Time.stk                   # `use Utils::Time`
    Path.stk                   # `use Utils::Path`
  bin/
    utils.stk                  # CLI front-end
  t/
    test_utils.stk             # all-surface assertions
    test_extra.stk             # assertions for the later sublib additions
  examples/
    discover.stk               # one call per sublib
    word_frequency.stk         # builtin-powered text pipeline
    config_merge.stk           # layered config via deep_merge_all + deep_get
    deploy_digest.stk          # cross-module pipeline (Time + String + Num + List + Path)

[0xFF] License

MIT.