# Small Collection Utilities

> Four small helpers worth carrying in a shared module, each with the one way it misleads — a symmetric set difference for diffing two id sets, a position index for constant-time lookups, a tolerant parse/serialize pair for timestamped tokens that returns null instead of throwing, and a translator that rewrites an external link into your own scheme. Use when a diff reports every item as changed, when a position lookup returns the wrong index for a repeated element, when one malformed line takes down a whole screen, or when pasting a link into a search box searches for the link.

- Skill: `maxrave-dev/small-collection-utilities` (Agent Skill)
- Install (CLI): `npx skillmds@latest add maxrave-dev/small-collection-utilities`
- Raw SKILL.md: https://api.skillmd.com/api/skills/maxrave-dev/small-collection-utilities/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: maxrave-dev (https://skillmd.com/u/maxrave-dev)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/maxrave-dev/small-collection-utilities

---


# Four helpers, four ways they mislead

Small enough to write from memory, which is exactly why nobody re-reads them once they are in.

## Symmetric difference

```kotlin
infix fun <E> Collection<E>.symmetricDifference(other: Collection<E>): Set<E> {
    val left = this subtract other
    val right = other subtract this
    return left union right
}
```

Answers "which elements are on exactly one side" — additions and removals in one pass, when
reconciling a stored list of ids against a freshly fetched one.

## Position index

```kotlin
fun <T> Iterable<T>.indexMap(): Map<T, Int> {
    val map = mutableMapOf<T, Int>()
    forEachIndexed { i, v -> map[v] = i }
    return map
}
```

Turns a repeated `indexOf` scan inside a loop into one pass plus constant-time lookups — the fix for
sorting one list into another list's order.

## A tolerant parse/serialize pair for timestamped tokens

For `<MM:SS.mm> word <MM:SS.mm> word …`, match **only the stamps** and slice the text between them:

```kotlin
val stampRegex = Regex("""<(\d{2}):(\d{2})\.(\d{2,3})>""")
val stamps = stampRegex.findAll(payload).toList()
stamps.forEachIndexed { index, match ->
    val (minutes, seconds, fraction) = match.destructured
    val fractionMs = fraction.toLongOrNull() ?: 0L
    val timeMs = (minutes.toLongOrNull() ?: 0L) * 60_000L + (seconds.toLongOrNull() ?: 0L) * 1_000L +
        if (fraction.length == 2) fractionMs * 10L else fractionMs      // 2 digits are hundredths
    val to = if (index < stamps.size - 1) stamps[index + 1].range.first else payload.length
    val text = payload.substring(match.range.last + 1, to).trim()
    if (text.isNotBlank()) out += Token(text, timeMs)
}
if (out.isEmpty()) return null      // the caller falls back to the coarser format
```

Write the serializer beside it, and **re-add whatever the parser stripped** — noticing which parser.
The line-level parse strips the space after the line stamp, and its writer puts that back:

```kotlin
if (lines.isNullOrEmpty() || syncType != RICH) return null    // refuse to write another format
return lines.joinToString("\n") { "[$stamp] ${it.payload}" }  // pad back the separator trim() ate
```

## External link to deep link

```kotlin
fun String.toAppDeepLinkOrNull(): Uri? {
    val trimmed = trim()
    if (!trimmed.startsWith("http://", true) && !trimmed.startsWith("https://", true)) return null
    val uri = runCatching { Uri.parse(trimmed) }.getOrNull() ?: return null
    val host = uri.host?.lowercase()?.removePrefix("www.") ?: return null
    …
    return Uri.parse("yourapp://watch?v=$id")     // hand to the handler you already have
}
```

## Traps

**Symmetric difference is set arithmetic, so membership is all it can answer.** Duplicates collapse,
order is gone, and an element that merely *moved* is on both sides and reports as unchanged. Run it
over models rather than ids and the answer depends on the model's `equals` — for a data class that
is every field, so editing a title reports the item as both removed and added. Diff the ids, then
compare matched pairs separately for content changes: two answers, two questions.

**A position index's assignment is unconditional, so the last occurrence wins.** A repeated element
maps to its final position and the map comes out shorter than the list, silently. Right for a list
of unique ids, wrong for anything else; build it with `getOrPut` if you want the first occurrence
instead. Same caveat as above on the key type — keying on a model means keying on its whole `equals`.

**A tolerant parser's trap is threefold, and the first two are silent.** *Matching stamp-and-text in
one regex ties the text's shape to the pattern:* punctuation, another angle bracket, a script the
character class never considered — all dropped, and the payload comes back looking merely short.
Slicing to the next delimiter has no opinion about the text at all. *The fraction's digit count is
its unit* — read the digits as a number without checking `length` and every two-digit stamp lands at
a tenth of where it belongs, early and plausible enough to look like a tuning problem rather than a
parsing one. *Every numeric field falls back rather than failing*, so "unparseable" and "zero" become
one answer — legitimate, but keep the **whole-payload** failure as `null` so the caller falls back to
a coarser rendering instead of losing the screen. The per-slice `trim()`s are restored by nothing,
safe only while the tokens stay a render-time value: serialize the parsed list back to text and the
words run together, so re-add the separator there too, and guard on the format tag either way, since
writing a payload you were not given parses into nonsense.

**The temptation with an external link is to handle it — the answer is to translate it.** Your
existing deep-link handler already knows the special cases (which id prefixes mean which kind of
collection, which need a prefix added), and a second handler starts as a copy and drifts the next
time one of those rules changes. Rewrite the URL into your own scheme and hand it to the same entry
point, so there is only ever one set of rules. Four details make the translator trustworthy: reject
non-`http(s)` text before parsing at all; return `null` for anything unsupported, so a pasted link
you do not handle becomes a search, not a failure; normalize the host before matching (`lowercase()`,
drop a leading `www.`), or the same link works or fails depending on where it was copied; and decide
the ambiguous case — a link carrying both an item and a collection — here, once, so every caller
agrees instead of guessing differently.

## Verifying it

The parse entry point should have exactly one failure mode, and it should be `null`:

```bash
PARSER=composeApp/src/commonMain/kotlin/com/maxrave/simpmusic/extension/RichSyncParser.kt   # your equivalent
grep -nE "return null|throw |!!|require\(|checkNotNull\(" "$PARSER"
```

Pass condition: every hit is `return null` — here, twice — never `throw`, `!!`, `require(` or `checkNotNull(`. Confirm it is the right file with `grep -n "Regex(\|fraction.length" "$PARSER"`: the timestamp regex and the fraction-length unit check both print.

Every caller of a translator that can return `null` has to say what `null` means:

```bash
FN=toAppDeepLinkOrNull
grep -rn -A3 "\.$FN()" --include="*.kt" .
```

The two collection helpers are only correct over distinct elements, so look at what reaches them:

```bash
grep -rn -B2 "\.indexMap()\|symmetricDifference" --include="*.kt" .
```

Per hit, confirm the receiver holds ids rather than models and cannot repeat. No hits at all is its
own finding: a helper nobody calls is one nobody has checked either.

