saropa_dart_utils 1.6.3
saropa_dart_utils: ^1.6.3 copied to clipboard
280+ extension methods and utilities for Flutter/Dart. Null-safe string, DateTime, List, Map, and number operations that eliminate boilerplate.
Changelog #
pub.dev - saropa_dart_utils
Published version: See field version in pubspec.yaml
1.6.3 #
Audit-tooling fix only — no library changes. log
Maintenance
Tooling
- Declaration parser in the publish audit (scripts/modules/audit.py) no longer misreads
const-list elements as declarations. A multi-line constructor call used as a list element (e.g.CrashFamilyCoverage(inside<CrashFamilyCoverage>[ … ]) opens a paren list that does not close on its own line, which the parser accepted as an undocumented declaration — falsely flagging the first entry ofkCrashCoverageAuditandkRuleRemediationsunder both "missing doc header" and "sparse code comments". Both_iter_declsand_method_rangesnow skip any line that begins while(/[nesting is still open (a<T>[ … ]literal or a split argument list);{is excluded so class/function bodies do not suppress the real members inside them.
1.6.2 #
Release-tooling hardening plus release-build input validation. The tooling work future-proofs the publish flow so a saropa_lints rule newly promoted to WARNING is caught before tagging instead of failing the tag-triggered publish one warning at a time (the v1.6.0 experience). The validation work converts assert-guarded preconditions on public-API input to if-throw guards so they hold in release builds, not just debug — a codebase-review sweep found that the dangerous subset degraded to a silent divide-by-zero, hang, or wrong result in production. log
Changed #
stableHashandHyperLogLogUtilsare now web-safe (stable_hash_utils.dart, hyperloglog_utils.dart) — both relied on 64-bit integer wrap that the web's 53-bit-doubleintmodel lacks (and shift operators truncate to 32 bits there), sostableHashproduced a different digest on web than the VM and the HLL estimate collapsed on web. The 64-bit FNV-1a multiply and the splitmix64 hash mixing / register-index-and-rank extraction are now computed in 32-bit limbs (16-bit split multiplies keep every intermediate under 2^53), so results are identical on every platform. The VM output is unchanged — the limb arithmetic reproduces the exact mod-2^64 result, verified against a BigInt ground truth over 200k (stableHash) and 500k (HLL) random inputs, so previously persistedstableHashdigests stay valid.stableHashgains a pinned-digest regression test.- Release-build precondition enforcement (18 files) — converted silent-failure
assertpreconditions on public-API input toif-throw guards (ArgumentError, orRangeErrorfor index/range args) so they run in every build, extending the v1.6.0FenwickTree/rolling_correlationprecedent to the rest of the library. Constructors validate through a static helper invoked in the initializer list, which keeps the throw out of the constructor body (soavoid_exception_in_constructorstays satisfied) and runs before any field initializer that would divide by the value.- Divide-by-zero / NaN:
SpatialGrid(cellSize),TimeDecayCounter(halfLifeMillis),TokenBucketRateLimiter(tokensPerSecond, capacity),binByWidth(bins, max > min),RateLimitSchedule(period),SlidingWindowRateLimiter(limit, window),TimeSeriesBuffer(bucketSizeMs, rawCapacity). - Hang / deadlock:
TaskScheduler(concurrency),BoundedWorkQueue(maxSize),ResourcePool(maxSize). - Silently-wrong result:
LruCache/MruCache/SizeLimitCachesize bounds;SegmentTree.update/query/valueAt(a negative index maps to a valid slot and corrupts the wrong leaf rather than throwing);IntervalEntry/IntervalTree.queryRange/hasOverlap(low > high);MultiIndexCollection(empty indexers);BkTree.search(negativemaxDistance);giniCoefficient(negative values). - Intentionally unchanged: the
constconstructorsQuietWindow,OpenWindow,RecurrenceSpeckeep theirasserts — aconstconstructor cannot throw and droppingconstwould be breaking;quantileBoundaries/binCountsalready degrade gracefully (return[]onbins < 2). - The matching precondition tests now expect
ArgumentError/RangeErrorinstead ofAssertionError.
- Divide-by-zero / NaN:
safeTempNamenow uses a cryptographically secure RNG (safe_temp_name_utils.dart) — was backed by a seedableRandom(), whose sequence is reproducible, so the generated temp-file names were predictable; an attacker who can guess the name can win a temp-file race or pre-create it. Switched toRandom.secure(). Also throwsArgumentErroron a non-positivelength(was silently returning an empty, always-colliding name).Map.getRandomListExceptaccepts an optionalRandom(map_extensions.dart) — previously shuffled with a fresh, non-injectableRandom(), making the result non-reproducible and untestable. Now takes an optionalrandom(defaults toRandom()), matching the injectable-RNG convention used elsewhere in the library. Backward-compatible (new optional parameter).hasInvalidUnicode/removeInvalidUnicodenow detect the real replacement character (string_analysis_extensions.dart) — the constant was56327(0xDC07, a lone low surrogate), not U+FFFD (65533) as the dartdoc said, so an actual�was never matched or removed. Fixed to65533; both methods now work as documented.- More grapheme-vs-code-unit slicing fixes (the same class as v1.6.0's
removeLastChar):removeFirstLastCharandremoveMatchingWrappingBrackets(string_manipulation_extensions.dart) counted code units but sliced with the grapheme-indexedsubstringSafe, so astral content shifted the result ('a😀b'.removeFirstLastChar()returned'😀b';'(😀)'kept the trailing bracket). Both now count graphemes, matchingremoveLastChars.removePrefix/removeSuffix(string_lower_extensions.dart) fed a code-unitprefix.length/suffix.lengthintosubstringSafe, dropping the wrong span when the prefix/suffix or content held astral chars ('😀ab'.removePrefix('😀')→'b'). Switched to code-unitsubstring, consistent withstartsWith/endsWith.
AsyncSemaphoreUtils.release()guards over-release (async_semaphore_utils.dart) — arelease()with no matchingacquire()(and no waiter) silently pushed_availableabovepermits, permanently letting the semaphore admit more thanpermitsconcurrent holders. It now throwsStateErrorinstead of corrupting the permit count.int.ordinal()— correct suffix for*11/*12/*13and negatives (int_string_extensions.dart) — the "teen" check was an absolute11..19window, so111.ordinal()returned'111st'(should be'111th'); likewise112/113/1011/213. And% 10on a negative gave the wrong suffix ((-21).ordinal()→'-21th'). Now tests the last two digits (11–13→th) and usesabs()for the ones digit, so111→'111th',101→'101st',(-21)→'-21st'. The class doc already promised negative support.flattenHierarchyno longer drops orphan-parent nodes (hierarchy_utils.dart) — a node whoseparentIdreferenced an id absent from the input was never a root and never reached by recursion, so it and its entire subtree silently vanished from the result. Such nodes are now treated as roots (level 0).formatNumberLocaleclampsdecimalPlaces(num_locale_utils.dart) —decimalPlaces > 20madetoStringAsFixedthrowRangeError. Now clamped to 20, matchingformatDouble.retryWithBackoffclamps the exponential shift (retry_utils.dart) —1 << (attempt - 1)was unclamped, so a largemaxAttemptsoverflowed the web's 32-bit shift and produced a wrong (small/negative) delay. Now clamped to<< 30, matchingexponential_backoff_utils/retry_policy_utils.
Deprecated #
String?.isNullOrEmptyandString?.isNotNullOrEmpty(string_nullable_extensions.dart) — convenient shorthand, but both defeat Dart's null promotion: the analyzer cannot see the opaque getter impliesthis != null, so afterif (text.isNullOrEmpty) return;(or insideif (text.isNotNullOrEmpty) { … }) the variable stays nullable and callers are pushed toward!. Prefer the long formstext == null || text.isEmptyandtext != null && text.isNotEmpty, which the analyzer understands and which promotetextto non-null in the guarded scope. Kept for source compatibility; deprecated to steer new code.
Added #
debounceCancelable/throttleCancelable(debounce_utils.dart, throttle_utils.dart) — variants ofdebounce/throttlethat return aCancelableCallback(a handle you invoke like a function and can alsocancel()). The plaindebounce/throttleclosures expose no cancel, so a pending timer fires even after the owner (e.g. a widget/controller) is disposed — a use-after-dispose and a Timer leak. The cancelable variants let callers drop the pending invocation on teardown. Non-breaking: the existingdebounce/throttleare unchanged; their docs now point to the cancelable variants.kRuleRemediationsrule-to-remediation mapping (rule_remediation_map.dart) — aconstdata table joining asaropa_lintscrash-prevention rule id to thesaropa_dart_utilspublic symbol that removes the runtime failure it flags (e.g.avoid_unsafe_reduce→sumBy,avoid_path_traversal→isPathSafe,geocoding_unchecked_first→firstWhereOrElse). This is the Saropa Suite Integration plan's R1 deliverable — the data that lets a sibling tool's quick fix upgrade from "enable rule X" to "enable rule X and useYfrom saropa_dart_utils." Ownership settled 2026-06-14 against the sibling changelogs: Lints owns rule→crash-signature, this package owns rule→util-symbol, joined onruleId. Every mapped symbol is pinned by a test that exercises it in compiled code, so alib/rename breaks the build rather than shipping a dead suggestion.kCrashCoverageAuditcrash-family remediation coverage audit (crash_coverage_audit.dart) — the Suite Integration plan's R3 deliverable. For each of the 12 stable crash families the suite recognizes at runtime (Log Capture'sCRASH_SIGNATURE_IDS), records whether this package covers it (coveredwith the owned symbol — e.g.state-error-no-element→singleOrNull,range-error-index→getOrNull,type-error-cast→castOrNull,format-exception→toIntNullable,concurrent-modification→forEachSnapshot) or is not remediable by a utility (notApplicable, e.g.late-init,out-of-memory,anr). No open gaps. Pinned by a test that asserts the family-id set equals the suite contract (a new upstream family fails until triaged) and exercises everycoveredsymbol in compiled code.List.forEachSnapshot(list_mutate_during_iteration_extensions.dart) — runs an action for each element of a point-in-time snapshot of the list, so the body may safely add to or remove from the list during iteration withoutConcurrentModificationError. Closes the suite'sconcurrent-modificationcrash family (R3 follow-up). Documents the snapshot semantics (added elements are not visited this pass; a removed element is still visited if it was present at snapshot time) and points pure conditional removal at the built-inremoveWhere.
Security #
isPathSafedirectory-traversal bypass fixed (path_validator_utils.dart) — the escape check measured depth from the filesystem root (depth < 0) while depth started at the root's own depth, so a path could climbrootParts.lengthlevels above the supplied root and still be reported safe.isPathSafe('../secret', 'home/user')returnedtruethough it resolves tohome/secret(outside the root). Now rejects the moment the path climbs above the root directory itself. If you relied on this for sandboxing, re-test: some previously-"safe" paths are now correctly unsafe.
Fixed #
pathExtension/pathWithoutExtensiononly treat a dot in the final segment as an extension (path_extension_utils.dart) — searching the whole path made a dot in a directory name look like the extension:pathExtension('/a.b/file')returned'b/file'andpathWithoutExtension('/a.b/c')returned'/a.b/c'→'/a'. Both now confine the dot search to the basename (and ignore a leading-dot dotfile).pathChangeExtensioninherits the fix.Map.renameKey/renameKeysno longer lose data or drop null values (map_more_extensions.dart) — they mutated the copy while iterating it, so a chained rename ({'a':'b','b':'c'}) overwrote'b'before renaming it ({'a':1,'b':2}→{'c':1}, value2lost); and anif (v != null)guard silently dropped entries whose value was null. Both now rebuild into a fresh map in one pass, preserving null values; a genuine target collision resolves last-wins (documented).JsonUtils.cleanJsonResponsestrips outer quotes around astral content (json_utils.dart) — used the grapheme-indexedsubstringSafewith a code-unit length, so a quoted value containing a non-BMP grapheme (e.g."🎉") kept its closing quote and then failed to decode. Switched to code-unitsubstring.formatFileSize(..., decimals: 0)keeps integer-part zeros (pad_format_utils.dart) —9728(9.5 KB) rounded to"10", and the unconditional0+$trailing-zero strip turned it into"1 KB". The strip now runs only after a decimal point.Map.deepMergereturns a fully independent result (map_deep_merge_extensions.dart) — nested maps/lists carried through unchanged were shared by reference with the inputs, so mutating the merged map mutated the receiver orother(also affectedcopyWithDefaults). Carried-through values are now deep-cloned.parseIpv4rejects non-canonical octets (ip_cidr_utils.dart) —int.tryParseaccepted a leading sign, surrounding whitespace, and leading zeros ('+1',' 1','01'), soparseIpv4('+1.2.3.4')parsed. Octets must now match a canonical decimal form (no sign/whitespace/leading-zero) — closes an octal-vs-decimal parser differential that can bypass IP allow/deny lists.isImageUri/fileExtensionuse code-unit slicing (url_extensions.dart) — fed a code-unitlastIndexOf('.')into the grapheme-indexedsubstringSafe, returning a wrong extension for filenames with astral characters. Switched to code-unitsubstring.parseQueryStringdecodes+as space (url_query_utils.dart) —Uri.decodeComponentdoes not translate+, so form/browser-produced query strings (a=hello+world) kept the literal+. Now+→space (a literal+arrives as%2B, sobuildQueryStringround-trips are unaffected).num.isNotZeroOrNegative/isZeroOrNegativehandle NaN (num_extensions.dart) —this != 0 && !isNegativeclassifiedNaNas positive. Rewritten asthis > 0/this <= 0, soNaNis correctly neither.roundToSignificantDigitsis correct at exact powers of ten (num_format_extensions.dart) — the decimal exponent was derived from a floatlog10that is fuzzy at powers of ten (e.g.log10(1000)≈ 2.9999997), mis-sizing the scale. Now nudges the exponent to the true integer with apowcomparison.FenwickTree(size)enforcessize >= 0in release (fenwick_tree_utils.dart) — the constructor used a release-strippedassert;size == -1slipped pastList.filled(0)into an unusable tree. Now throwsArgumentErrorvia a static validator (v1.6.0 fixed only the methods).prettyPrintindents entries correctly (debug_utils.dart) — keys/items were prefixed with the parent level's pad, so they were one level too shallow (top-level entries had no indent). Entries are now indented one level deeper than the closing brace.rangeDoubleavoids float drift and a zero-step infinite loop (debug_utils.dart) — computedx += step(accumulating error, so a0.1step could drop/add the endpoint) and would loop forever forstep == 0. Now guardsstep == 0and computes each element asstart + i*step.
Maintenance
Tooling
- CI: added a standalone
release_gateworkflow (.github/workflows/release_gate.yml) that runsdart pub publish --dry-runon every push/PR to main — the same command the tag-triggeredpublish.ymlenforces (it runsdart analyzeinternally and exits 65 on any warning). Blocking warnings now surface at PR time, before the irreversible version tag, instead of one-per-tag-round. It is a separate workflow rather than a job inci(main.yaml) because that workflow is disabled, which would have made the gate inert. - Release script (
scripts/modules/workflow.py): the local analysis gate now fails on WARNING-severity findings, not just errors. Becausedart pub publishexits 65 on a single warning, a warning that previously passed this gate still blocked the publish; matching the semantics locally catches it before tagging. pubspec.yaml: pinnedsaropa_lintsto exact13.12.7(was^13.12.7). A caret range let CI's fresh resolve pull a newer patch that promoted a rule to WARNING while the older local lock showed nothing — the version drift behind the whack-a-mole.- Doc-header audit (scripts/modules/audit.py) no longer false-flags a public member as "missing doc header" when a multi-line annotation sits between its dartdoc and the declaration. The upward walk that looks for the
///block now tracks paren depth, so the interior lines of a split@Deprecated( 'msg' 'msg' )(which end with', not@/)/,) are skipped to the opening@Name(instead of aborting the walk. This was reporting the deprecatedisNullOrEmpty/isNotNullOrEmptygetters as undocumented when both carry full dartdoc. suggest_saropa_utilsscanner (tool/suggest_saropa_utils_lib.dart) rebuilt against the reallib/API. Removed detectors that recommended utils that do not exist (orZero,orNow,toIntOr, the misspellednotNullOrEmpty) — they suggested non-compiling code — and detectors that pushed the null-promotion-defeatingisNullOrEmpty/isNotNullOrEmpty/isNullOrZerogetters. Replaced with 45 detectors each audited for a real, flow-analysis-safe target (capitalize,takeLast,dropLast,lastOrNull,ensurePrefix/ensureSuffix,countWhere,whereNotNull,containsAny,getEverythingBefore/After,compressSpaces,orEmpty,isSameDay,startOfDay/endOfDay,addDays/addHours/addMinutes/addMonths/addYears,isLeapYear,isWeekend,flatten,none,containsAll,sumBy,invert,isNumeric,isPalindrome,removeAll,wordCount,percentageOf,lerp,isInteger, …), plus a reverse detector that flags use of the deprecated getters and points back at the explicit form.
Documentation
- Doc accuracy:
hexToInt(hex_utils.dart) no longer claims it "Prints a warning to the debug console" — it returnsnulland prints nothing (the v1.6.0 cleanup fixed only the example, not the prose).varint(varint_utils.dart) now documents that values beyond 32 bits round-trip only on the Dart VM — on web,intis a 53-bit double and the shift operators truncate to 32 bits. toPercentage(double_extensions.dart) doc examples used the wrong parameter name (roundDown:→doRoundDown:); they now compile.topKIndices(top_k_heap_utils.dart) doc corrected: it keeps the k best in a bounded sorted buffer, not a binary heap.withTimeout(timeout_policy_utils.dart) documents that anullfallback is read as "no fallback" (rethrow), so a nullableTcannot usenullas the recovery value — use the required-fallbacktimeout_fallback_utilsinstead.sanitizeHtml/stripHtmlTags(html_sanitizer_utils.dart) docs corrected: it is an HTML-to-plain-text reducer, NOT an allowlist sanitizer (the old doc claimed "allowlist tags/attributes" it never implemented). Added a caveat that its regex tag-stripping is not a security sanitizer and must not be used to produce HTML for re-insertion.
Refactoring
_satisfiesCaret(dependency_resolver_utils.dart) rewritten from a nested ternary to a chained conditional (project style rule). No behavior change.
Tests
suggest_saropa_utilsscanner — coverage expanded from ~17 to 98 cases (suggest_saropa_utils_test.dart) — one positive test per detector confirming it fires on its canonical hand-rolled snippet, real-world variant cases (capitalize via string interpolation and viasubstring(0, 1), extra-whitespacesublist,this-receiverlastOrNull, parenless leap-year), false-positive guards (correct long-form guards, already-using-the-util, literal-boundsublist,wherewithout.length, unguarded division,anywithout negation), and scanner edge cases (empty/whitespace input, multi-hit lines, 1-based line numbers). The expansion caught a real defect: thecountWheredetector's regex stopped at the first)and so never matched a lambda predicate (.where((e) => …).length) — fixed to allow one level of nested parens. Thecapitalizedetector was broadened to also recognize the string-interpolation andsubstring(0, 1)forms found in common Dart idioms.splitCapitalizedUnicodeminLengthmerging now covered (string_text_extensions_test.dart) — the merge branch (the method's most complex path: fuse adjacent parts shorter thanminLength) had no tests. Added 10 cases pinning the real behavior:minLength: 1is a no-op, partial vs full fusion, a part already at the floor staying split, thestraße/ÖsterreichUnicode merge, thesplitNumbersdigit-to-letter split ('Area51TestSite'→['Area', '51', 'Test', 'Site'], four parts — the library's regex separates digit-from-letter, unlike the originating app copy), andsplitBySpacerunning after the merge so it is not subject tominLength. Closes the coverage gap from the 1.6.x utility migration harvested out of Saropa Contacts.- Direct coverage for the named weekday-of-month wrappers (month_weekday_named_extensions_test.dart) —
firstThursday/firstFriday/firstSaturday/secondFriday/secondSaturday/lastFriday/lastSaturdaypreviously had no test naming them directly (their contract was only covered transitively through the bulk non-null sweep). Added one sample-date case per wrapper pinning both the resolved date and itsweekday, so a regression in a single wrapper's ordinal/weekday argument is caught by name. - Rule-to-remediation mapping pinning test (rule_remediation_map_test.dart) — exercises each
kRuleRemediationssymbol in compiled code (an empty-collectionsumBy, a no-matchfirstWhereOrElse, a traversalisPathSafe, etc.), so a renamed or unexported symbol fails the build. Also asserts two-way drift fails (a mapped symbol with no probe, or a probe for an unmapped symbol), rule ids are unique, all fields are non-empty, and everysourcenames a.dartfile. - Crash-coverage audit pinning test (crash_coverage_audit_test.dart) — asserts
kCrashCoverageAudit's family-id set equals the suite'sCRASH_SIGNATURE_IDScontract (an added/removed upstream family fails the test until reconciled), exercises everycoveredsymbol in compiled code, enforces the status invariants (coveredcarries symbol+source, others carry neither), and pins the open-gap set to empty so a coverage regression or a new uncovered family is noticed. forEachSnapshotedge-case tests (list_mutate_during_iteration_extensions_test.dart) — removal during iteration without throwing, the contrasting plainfor-inthat DOES throwConcurrentModificationError, added elements not visited this pass (bounded against an infinite loop), a removed element still visited if present at snapshot time, plus empty / single / no-mutation / nullable-element cases.- Release-audit cleanup ahead of tagging 1.6.2:
formatNumberLocale(num_locale_utils.dart) — reworded thedecimalPlaces-clamp comment so it no longer reads as commented-out code (it contained aname(args)call form that trippedprefer_no_commented_out_code). No behavior change.AsyncSemaphoreUtilstests (async_semaphore_utils_test.dart) — suppressedprefer_setup_teardownwith a rationale comment: each test constructs its semaphore with the permit count that IS the scenario under test (1 to serialize, 2 to bound concurrency), so a sharedsetUp()would hide the parameter each case exercises.
1.6.1 - 2026-06-12 #
Clears every pub.dev "Pass static analysis" point deduction (was 40/50): the seven dangling library doc comments pana reported plus the other eleven plugin findings it counts in the same check. dart analyze is now fully clean. log
Fixed #
stats/rolling_correlation_utils.dart: replaced two release-strippedasserts withif-throw guards (nowArgumentError) so equal-length andwindow >= 2preconditions are enforced in release builds, not just debug.collections/skip_list_utils.dart: replaced a!null-assertion inaddwith an explicit invariant guard that throwsStateError, and switched three[0]reads tofirstOrNull(added thepackage:collectionimport) so neither the unsafe-collection nor null-assertion lint fires.
Maintenance
Lint
- Added
library;after the dangling file-level doc comment in:base64/gzip_codec_io.dart,base64/gzip_codec_stub.dart,object/pipe_compose_utils.dart,object/pipe_utils.dart,object/shallow_copy_utils.dart,testing/debug_utils.dart,url/url_encode_utils.dart. - Reworded three explanatory comments (
num/num_more_extensions.dart,datetime/date_time_week_extensions.dart,validation/jwt_structure_utils.dart) so no line begins with anidentifier.membertoken, which the commented-out-code lint misread as dead code. No code or behavior change.
Tests
parsing/varint_utils_test.dart:expect(encoded.length, 10)→expect(encoded, hasLength(10))for a proper matcher and clearer failure output.
Refactoring
collections/fenwick_tree_utils.dart:valueAtconverted to an expression body (=>).
1.6.0 - 2026-06-12 #
Adds 35 advanced utilities from the roadmap — order-statistic trees, multi-source graph search, time-series analytics, calendar/billing helpers, and async cancellation; then a second batch of range-query trees, approximate string search, spatial indexing, async write caches, graph simplification/serialization, and rate-limited scheduling — plus a null-year-tolerant day-count helper. Then a full-project correctness audit of all 476 library files: ~40 algorithm/crash/concurrency bug fixes (incl. a semaphore permit race and two process hangs), ~30 documentation-accuracy corrections, 40 new regression tests, and a per-method audit-date stamp on every method. All fixes preserve existing signatures (backward-compatible). log
Maintenance
Lint, docs, and tooling
-
Added a tree-shaking checklist to the README Installation section — documents that the barrel import (
saropa_dart_utils.dart) costs the same shipped size as leaf imports (AOT/dart compile jskeep only reachable code), that extensions shake per method, that the package adds novm:entry-point/dart:mirrors/eager top-level side effects, and the two caveats (tree-shaking is release-build only;pub getdownload size is separate from app-binary size). -
Rewrote
HebrewDateConverter.getMonthNameleap-year branch as a Dart 3switchexpression (hebrew_date_converter.dart) — clearsprefer_returning_conditional_expressions(saropa_lints) without introducing the nested ternary the project'sdart.mdbans. Behavior unchanged (all 100 converter tests pass): the default arm covers both the 1-5 and 8+ direct-index months; 6 → Adar I, 7 → Adar II. -
Suppressed a
prefer_setup_teardown(saropa_lints) false positive in color_light_test.dart with a documented// ignore:— the flagged per-group arrange is a known false positive fixed upstream in saropa_lints 13.12.4 (raises the duplicate threshold when a file-levelsetUpalready exists); the project pins 13.12.3, so the fix has not arrived. Tests stay explicit per the project's clarity-over-DRY testing rule. -
Suppressed
require_timezone_display(saropa_lints) in the twointlclock-rendering files (date_time_intl_time_display_extensions.dart, date_time_intl_display_render.dart) with a documented// ignore_for_filereason. These are locale clock-rendering primitives where the caller owns the timezone-display decision; two flagged sites are provable false positives (aDateFormat.jm()read only for its.patternstring and never formatted, and a seconds-onlyDateFormat('ss')whose field is timezone-invariant). Filed upstream as a saropa_lints false-positive bug report. -
Suppressed
prefer_correct_callback_field_name(saropa_lints) in backtracking_utils.dart with a documented// ignore_for_filereason. The four flagged fields (choices,apply,isComplete,isValid) are pure strategy callbacks — a candidate generator, a state transform, and two predicates — not UI event handlers, so theonXxxconvention the rule enforces would misname them; they are also public fields of a published API, so renaming would be breaking. -
Suppressed an
avoid_accessing_collections_by_constant_index(saropa_lints) false positive in skip_list_utils.dartvalueswith a documented inline// ignore:reason. The flaggednode.forward[0]is the level-0 successor link in the iterator's list walk —nodeis reassigned every iteration, so it is a genuine traversal, not the wasteful fixed-element re-read the rule assumes. This was the lone Warning-severity issue that failed thedart pub publish --dry-runanalyze gate and blocked the v1.6.0 publish (exit 65);.firstwas rejected as the alternative because it trades the warning foravoid_unsafe_collection_methods. -
Added
/build/to .pubignore. A root.pubignoreoverrides.gitignorefor pub, so.gitignore'sbuild/rule stopped applying and the CI runner'sflutter testartifacts (a 64 MBtest_cachedill plus native/unit-test assets) were bundled into the published tarball, bloating the v1.6.0 archive to 21 MB. Excluding/build/ships the package substantially lighter. -
Fixed a flaky test in common_random_test.dart that intermittently failed the publish
Run testsgate. "unique instances with default seed" compared a singlenextInt(100)from two RNGs, which collide ~1% of the time even when seeded differently — a latent ~1% spurious-failure rate. It now compares a 10-draw sequence (collision probability 100^-10) and delays 2 ms (not 1) to reliably cross a millisecond seed boundary. Library behavior unchanged; the test was at fault. -
Disabled the
avoid_string_substring(saropa_lints) rule in analysis_options.yaml and analysis_options_custom.yaml. A newersaropa_lints(pulled by the CI fresh-resolve, since the lock is untracked) promoted this heuristic to WARNING severity, which failsdart pub publish --dry-runwith exit 65. The library has 49 deliberate, bounds-checkedsubstring()calls across parsers, hex/color, and URL templates where indices come fromRegExpmatch offsets or prior length checks — all provably in-range. Auditing/suppressing each site individually is impractical and the rule adds no safety here, so it is disabled project-wide; re-enable with a per-site audit if desired. This was the recurring blocker that failed the v1.6.0 publish across multiple retries (each dry-run surfaced a different substring site). -
Expanded the dartdoc on
MapExtensions.countItems<K, V>(map_extensions.dart) to enterprise-grade: documents the post-deduplicationSetcount, lazy-iterable materialization, content-irrelevance (emoji/combining-mark/nulleach count as one element), order-independence, and non-mutation, plus a worked example. No behavior change. -
Removed the duplicated per-member
/// Hex range:doc comments fromUnicodeClassType(unicode_class_type.dart), bringing the file under the 200-line limit (346 → 147). The inclusive code-point ranges already live once inunicodeClassRanges(unicode_class_blocks.dart) — the single sourcefindUnicodeClassTypeactually reads — so the enum docs were a drift risk. The enum header now points auditors to that table. No enum members changed (105, same order); no behavior change. -
Cleared the open
saropa_lintsanalysis-server diagnostics across eight files, all behavior-preserving (analyze + affected suites green):ColorUtils.getColor(material_color_utils.dart) now reads the non-nullableMaterialColor.shadeNNNgetters instead ofcolor[NNN]!, removing all tenavoid_null_assertionhits and the null-assertion caveat from its dartdoc; threeif/elsevalue-returns inHebrewDateConverter(hebrew_date_converter.dart) collapse to single (non-nested) conditional expressions; thetwoDigitshelper comment inDurationClockFormatExtensions(duration_clock_format_extensions.dart) becomes a///doc comment; four prose WHY-comments thatprefer_no_commented_out_codemisread as code (they led withtoInt()/sublist(...)/?./ a bare// Tens./// Units.label) were reworded, not deleted, in hebrew_date_converter.dart, double_aspect_ratio_extensions.dart, list_string_extensions.dart, and text_direction_parse_utils.dart. Two adjacent diagnostics were deliberately left as-is because resolving them conflicts with this project's own.claude/rules: collapsing the remainingmonth==6branch would force a nested ternary (banned bydart.md), and furthersetUpextraction in the lighten suite would force unrelated tests to share state (againsttesting.md's "Clarity Over DRY").
Fixed #
-
FenwickTreebounds checks now enforce in release builds (fenwick_tree_utils.dart) —update,prefixSum, andrangeSumguarded their index/range arguments withassert, which the Dart compiler strips from release builds (avoid_assert_in_production). In production a negativeindexpassed toupdatewould makei & -i == 0and spin the update loop forever (a hang, not a no-op); an out-of-range index toprefixSum/rangeSumwould return a silently wrong sum. All three now throwRangeErrorso the checks run in every build.valueAt's redundant assert is removed (it delegates to the now-validatedrangeSum). The two bounds tests now expectRangeErrorinstead ofAssertionError. -
removeLastChar()/removeLastChars(count)now count by grapheme cluster (string_manipulation_extensions.dart) — both methods bounded by code-unitString.lengthbut sliced with the grapheme-indexedsubstringSafe, an incoherent hybrid: it subtracted a code-unit count from a code-unit length, then used the result as a grapheme index. The visible effect depended on how many code units each trailing grapheme spanned — e.g.'a😀'.removeLastChar()returned'a😀'unchanged (the emoji's two code units left the grapheme index past the end), and the dartdoc's "counts UTF-16 code units / can split a cluster" claim was the opposite of what the code did. Both now count user-perceived characters: thecountargument is a count of visible characters, a trailing emoji or base+combining-mark sequence is removed whole, and the dartdoc matches.'a😀'.removeLastChars(1)→'a'; decomposed'Café'.removeLastChars(1)drops the whole accented cluster →'Caf'. Pure-ASCII behavior is unchanged. The three pre-existing emoji/combining-mark tests are rewritten to pin the coherent grapheme results, plus a newremoveLastCharemoji case. -
pathRelativenow emits leading..for up-traversal (path_join_utils.dart) —pathRelative('a/b/c', 'a/b/d')returned'd'instead of'../d'. The function computed the correct climb-out segments but joined them throughpathJoin, which treats a leading..as a pop with nothing above it and discards it. It now joins the already-clean segments directly, so any relative path that must ascend out ofbaseis correct ('a/b/c/d'→'a/x'yields'../../../x', pure ascent yields'../..'). Identical paths still yield'';pathJoin/pathNormalizepop semantics are unchanged for their other callers. Pinned by three new up-traversal tests. -
Project audit pass 7 — caching / int / double / json / bool / base64 + singletons (~43 files):
formatPrecision(double_extensions.dart) clampsprecisionto 0–20 (the rangetoStringAsFixedaccepts) instead of throwing a RangeError on a negative or >20 value.- Doc accuracy:
toBoolJson(json_type_utils.dart) documented as a truthy coercion that returnsfalse(notnull) for unrecognized non-null input — only anullargument yieldsnull(matching its tested behavior);int.ordinalexample114th(was101th);hexToIntexamples drop the false "prints a warning" claim;double.leastOccurrencescomment fixed (was a copy-paste of "highest"). - The cache implementations (LRU/MRU/TTL/size-limit/write-through), UUID v4 version/variant bits, HTML entity decoding, color packing/WCAG, and base64 round-trips were all audited and verified correct.
- 1 new regression test; per-method audit-date stamps across all files.
-
Project audit pass 6 — iterable / map / list / validation / url / object / niche (97 files):
jwtPayload(jwt_structure_utils.dart): two fixes — the base64url padding calc appended a spurious====block when the payload length was already a multiple of 4 (rejecting otherwise-valid tokens), and the bytes were decoded as raw code units instead of UTF-8 (mojibake for multi-byte claims). Now(block - len%block) % blockpadding andutf8.decode.mapDiff(map_diff_utils.dart): a genuinely-nullvalue (nullable V) is now reported as added/changed/removed instead of being skipped as if absent (key-presence based, not value-null based).lastWhereOrElse(iterable_first_last_extensions.dart) andrunLengthEncode(run_length_utils.dart): a matched/prevnull(nullable T) is no longer mistaken for "no match"/"no run" — the?? orElseandprev == nullsentinels are replaced.truncateToByteLength(niche_more_utils.dart): counts UTF-8 bytes per RUNE, so a non-BMP emoji is 4 bytes (not 6) and is never split into an invalid surrogate half.unflattenKeys(map_flatten_extensions.dart) andsetNested(map_nested_extensions.dart): a scalar/branch key collision now actually stops instead of writing the leaf into the wrong (outer) map.- Guards:
takeLast(negative n → empty, was RangeError);randomAlphanumeric(non-positive length → '', was a crash);parseQueryString(malformed percent-escape → literal text, was FormatException);getRandomListExcept(negative count clamped);shapeString(result never exceeds maxLength, was returning'...'for maxLength 2). - Doc accuracy:
map_initials_sortexample corrected;list_top_k"(partial sort)" → full-sort note;truncateToByteLengthdoc updated to code-point boundaries. - 5 new regression-test groups; per-method audit-date stamps across all 97 files.
-
Project audit pass 5 — string corrections (lib/string, 78 files):
SoundexUtils.encode(soundex_utils.dart) now treats vowels (A,E,I,O,U,Y) as adjacency-breakers (resetting the run) while H/W stay transparent — same-coded consonants separated by a vowel are re-coded, soGauss→G200instead ofG000.substituteTemplate(string_template_extensions.dart) does a single regex pass so a value containing another key's placeholder is not re-substituted (was chainedreplaceAll).markdownToPlainText(markdown_plain_utils.dart) strips image syntaxbefore the link rule, so it no longer leaves a stray leading!.chunkText(text_chunk_utils.dart) forces at least one char of forward progress, sooverlap >= maxCharsterminates instead of looping forever / overflowing the buffer.parseSearchQuery(search_query_parser_utils.dart) skips a bare-token instead of emitting an empty negated term.extractUrlsWithContext(url_extract_utils.dart) trims trailing sentence punctuation the greedy match swallowed (https://a.com.→https://a.com), leaving brackets intact for paren-bearing URLs.- Doc accuracy:
truncateMiddleandredactPhoneexample outputs corrected;String.reverseddocumented as rune-based (breaks grapheme clusters);template_engine_utilsno longer claims conditionals;textFingerprintdocumented as an order-sensitive identity hash, NOT a simhash (its Hamming distance is not a similarity measure). - 6 new regression-test groups; per-method audit-date stamps across all 78 files.
-
Project audit pass 4 — async & parsing corrections (lib/async 34, lib/parsing 37):
AsyncSemaphoreUtils(async_semaphore_utils.dart) fixed a permit double-counting race:releaseincremented_availableeven when handing the permit directly to a waiter while the wokenacquiredecremented again, so a fast-path acquirer in the wake-up gap could admit a second holder and drive the count negative.releasenow hands off without touching the pool.- Hang guards:
mapBatched(batch_async_utils.dart) treats a non-positive batch size as 1 instead of looping forever;raceFirst(race_cancel_utils.dart) fails fast on an empty producer list instead of returning a never-completing future;retryWithJitter(retry_policy_utils.dart) guards a zero jitter (nextInt(0)RangeError) and clamps the backoff shift;bufferCount/windowCountclamp a non-positive count to 1. SemverUtils.compareTo(semver_utils.dart) now compares pre-release identifiers per semver §11 (numeric-vs-numeric numerically, numeric below alphanumeric, longer ranks higher) instead of a plain string compare that madealpha.2 > alpha.10.encodeVarint/decodeVarint(varint_utils.dart) round-trip negative and >2^35 values: encode uses a logical shift + mask test (negatives emit the full 10-byte form), decode's cap is raised to 64 bits.parseHexColor(hex_color_utils.dart) rejects an embedded non-hex char instead of stripping it and parsing the coincidentally-valid remainder (#a1z2c3d→ null).parseChangelogSections(changelog_section_utils.dart) slices with code-unitsubstring, not the grapheme-indexedsubstringSafe, so emoji/non-BMP content no longer misaligns section bodies.parseNestedQuery(nested_query_parser_utils.dart) skips a scalar/nested key collision (a=1&a[b]=2) instead of leaking the nested leaf to the root.- Doc accuracy:
CircuitBreakerUtilsdocumented as two-state (no real half-open gating);memoizeFuturenotes failures are cached permanently;compareVersionswarns it ignores pre-release suffixes;raceFirstheader no longer claims it cancels losers. - 5 new regression-test groups; per-method audit-date stamps across all 71 files.
-
Project audit pass 3 — datetime corrections (lib/datetime, 57 files):
- Two unbounded-loop guards:
expandRecurrence(recurrence_iterator_utils.dart) now abandons an impossible rule (e.g.FREQ=YEARLY;BYMONTH=2;BYMONTHDAY=30) after a long empty run instead of hanging even under.take(n);fillMissing(timeseries_gap_utils.dart) returns the input sorted for a non-positive interval instead of spinning forever / OOM. - DST / calendar-field correctness:
heatmapGrid/_weekStart(calendar_heatmap_utils.dart),dayOfYear/numOfWeeks(and thusweekOfYear/weekNumber) (date_time_calendar_extensions.dart),getNthWeekdayOfMonthInYear(date_time_extensions.dart), andsplitByMonth(period_split_utils.dart) now step dates with calendar fields / UTC date-only endpoints instead ofDuration(days:n)on local time, which drifted off midnight (and one day off, fordayOfYear) across DST transitions. parseIsoWeekString(date_time_week_extensions.dart) now rejects a week the year does not have (e.g.2025-W53) by validating the week's Thursday-year, instead of returning a date in the following ISO year.isAnnualDateInRange(date_time_comparison_extensions.dart) no longer matches a Feb-29 annual date against Mar 1 in non-leap years (DateTime silently rolls Feb 29 over).- Doc accuracy:
getEmojiDayOrNightno longer claims error-handling/logging it does not do;convertDaysToYearsAndMonthsexamples corrected (365 days is 11 months, not 1 year;45with remaining days is15, not14). - 6 new regression tests; per-method audit-date stamps across all 57 files.
- Two unbounded-loop guards:
-
Project audit pass 2 — collections corrections (lib/collections, 64 files):
kmeans2D(kmeans_utils.dart) now seeds centroids with maximin (greedy k-means++) spreading instead of placing all k atpoints[0]— the old seeding collapsed the output to at most two clusters for any k because identical centroids never diverge under Lloyd's algorithm.bucketByTime(time_bucket_utils.dart) andTimeSeriesBuffer(timeseries_buffer_utils.dart) now FLOOR the epoch-to-bucket division instead of truncating toward zero, so pre-1970 (negative-epoch) timestamps land in the correct bucket.bucketByTimealso returns empty for a non-positive bucket width instead of dividing by zero.BloomFilterUtils(bloom_filter_utils.dart) sizes the bit array withdart:math.loginstead of a hand-rolled natural-log approximation that mis-sized the filter.DifferenceArrayUtils.addRange(difference_array_utils.dart) now ignores a reversed range (l > r) as a no-op instead of silently corrupting the recovered array.- Doc accuracy:
StreamQuantileUtilsdocumented as the exact O(n) estimator it is (not the "P²-style fixed-memory" the title implied; removed the dead no-op buffer sort);PriorityMapUtilsdocumented as draining buckets in priority-insertion order, NOT by comparing key value;toColumnardocumented as using the first row's keys as the schema;topKIndicesdocumented as returning an unspecified order;pivot_unpivot_utilsnotesunpivotis not implemented. - 4 new regression tests; per-method audit-date stamps across all 64 files.
-
Project audit pass 1 — graph & num algorithm corrections (stats/graph/num categories):
floydWarshall(floyd_warshall_utils.dart) now seeds the distance matrix with the minimum edge weight per(i, j), not the last one — parallel edges in a multigraph and positive self-loops no longer overwrite a shorter distance or the diagonal0.dagSchedule(dag_scheduler_utils.dart) now appliespriorityas a tiebreaker among nodes that are simultaneously ready (priority-aware Kahn), instead of sorting the whole topological order by priority — the old form could place a dependency after its dependent, breaking topology.douglasPeuckerIndices(line_simplify_utils.dart) perpendicular distance now divides by the chord length (sqrt(dx²+dy²)), not the squared length —epsilonis once again a true distance tolerance; the degenerate zero-length chord uses Euclidean distance.dijkstraDistances/dijkstraWithParents/astar/bfs/dfs/criticalPathDistancesnow guard an empty graph or out-of-range start/source/goal instead of throwingRangeError(astar returnsnull, matching its no-path contract).nextPowerOfTwo(num_more_extensions.dart) bit-smear now reaches>> 32, fixing wrong results for 64-bit inputs above 2³².lcm(math_utils.dart) divides before multiplying to avoid 64-bit overflow on large operands.- Doc accuracy:
outlierIndicesByMADno longer claims the modified z-score cutoff (it omits the 0.6745 constant — now documented);divideSafedrops the impossible "or null" divisor note;NumUtils.generateIntListdrops the "prints a warning" claim (it does not print). - 6 new regression tests pin the corrected behaviors. Per-method audit-date stamps added across stats/graph/num doc headers.
-
List<String>helper tests pinned to real Dart casing/trim behavior (list_string_extensions_test.dart) — corrected two test expectations that asserted behavior Dart does not have, plus the matching source dartdoc. (1) The eszett case test claimed['straße'].toUpperCase()yields['STRASSE'], but Dart'sString.toUpperCase()is a 1:1 code-point map that leaves'ß'unchanged ('STRAßE'); the test and theListStringExtensions.toUpperCasedartdoc (which wrongly stated'ß'->'SS') now state the real no-expansion behavior. (2) TheremoveTrimmedEmpty(trim: false)test expected[' a ', ' ']to drop the whitespace-only' ', contradicting the documentednullIfEmpty(trimFirst:false)contract (and the sibling tests at the same group) under whichtrim:falsedrops only a literal''and keeps' '; the test now asserts both entries survive. Source behavior was correct in both cases — only the wrong expectations and the stale dartdoc changed. -
ComputeStreamTransformernon-sendable-closure test no longer hangs the suite (compute_stream_transformer_test.dart) — the test passed a closure capturing aStreamController(a non-sendable native receive port) throughcompute()and asserted the stream surfaced an error. That "non-sendable closure rejects" behavior is a real-isolate (on-device) contract that is unobservable in the Flutter test VM: sending such a closure leaves a dangling_RawReceivePortthat never closes, so the test isolate could not quiesce and the case ran to the 30-second timeout — the platform sensitivity the spec already flags. Reworked the test to assert the observable, supported half of the contract (a top-levelComputeCallbackcomputes correctly) and documented why the closure-rejection path is intentionally not exercised here. Source unchanged — it already propagates compute failures correctly (covered by the passingonError/ rethrow tests). -
DateTime.getSimpleRelativeDay/getRelativeDayResultnow count whole calendar days across a DST spring-forward (simple_relative_day_utils.dart) — the date-only normalize built LOCAL midnights, so on a spring-forward day (e.g. Mar 10→11 2024 in a US zone, a 23-hour local day)difference().inDaystruncated a genuine one-calendar-day gap to0, misbucketing tomorrow as today. The normalizer now builds UTC midnight from the calendar fields, so two normalized dates are always an exact multiple of 24 hours apart and the day count is correct regardless of DST or the receiver's UTC-vs-local kind. -
String.removeEndnow strips a code-unit suffix that falls inside a grapheme cluster (string_manipulation_extensions.dart) —removeEndmatched the suffix with UTF-16endsWithbut sliced with the grapheme-awaresubstringSafe, which reinterpreted the code-unit cut indexlength - end.lengthas a grapheme index and refused to split a base+combining-mark cluster, leaving the suffix un-stripped. So('e' + U+0301).removeEnd(U+0301)returned the unchanged'é'instead of'e'. Switched the slice to plainString.substring— afterendsWithis true the cut point is a guaranteed-valid code-unit boundary, so it never throws — making the strip consistent with the documented UTF-16 code-unit contractremoveEndNullableinherits. ASCII/BMP suffixes are unaffected. -
TextDirectionParseUtils.tryParsenow rejects a BOM-prefixed token (text_direction_parse_utils.dart) — Dart'sString.trim()strips U+FEFF (BOM / zero-width no-break space) as whitespace, so'ltr'was incorrectly parsing asltrinstead ofnull. The SPEC pins BOM as NOT whitespace (it is a formatting char, unlike NBSP / thin / ideographic space, which stay trimmed), so a private_trimhelper now re-attaches any leading/trailing BOM thattrim()removed, keeping the token unrecognized while preserving real-Unicode-whitespace trimming. Aligns the source with the existing zero-width-space (U+200B) handling. -
DateTime.toDateFormatnow returns''for an invalid pattern (date_time_intl_display_extensions.dart) — the documented "invalid pattern →'', never throws" contract relied solely on a try/catch, but intl'sDateFormatdoes NOT throw on an unrecognized field letter such as'q': it echoes the letter as literal text ('q'.toDateFormat('q')returned'q'), so the catch never fired. Added an up-front_patternHasUnknownFieldguard that rejects any unquoted ASCII letter outside intl's implemented field set (GyMkSEahKHcLQdDmsvzZ, sourced from intl's own pattern matcher), returning''before formatting. Letters inside single-quoted literal sections are exempt, so a literal pattern like'q'(quoted) still renders. -
Corrected the
compareAgesinstant-equality test (date_time_compare_age_extensions_test.dart) so it constructs the UTC and local operands from ONE instant (utc.toLocal()) instead of buildingDateTime.utc(2020, 1, 1)andDateTime(2020, 1, 1)independently. The old form assumed they were the same moment, butDateTime(...)is wall-clock local time — a different absolute instant fromDateTime.utc(...)on any host not at UTC+0 — so the instant-basedcompareTocorrectly returned-1, failing the assertion. The source comparator was correct; the test fixture was wrong. -
Corrected the
ColorUtils.getColorshade500 test (material_color_utils_test.dart) so it compares ARGB values (toARGB32()) instead of objects.getColor(shade500, Colors.red)returns the plainColorat[500]; the old assertion compared it to theColors.redMaterialColorobject, which never equals a plainColorbecause the two are different runtime types even when the tone is identical. Now both sides are reduced to theirtoARGB32()integer (replacing the deprecated.value), matching the spec intent that shade500 is the swatch's primary tone. The source was correct; the test fixture was wrong. -
Fixed
findUnicodeClassType(value, ignoreWhitespace: false)(unicode_class_utils.dart) to classify whitespace runes into their Unicode block instead of returningnull. The function calledvalue.trim()unconditionally, and Dart'strim()strips Unicode space separators (NBSP U+00A0, en-space U+2002, ideographic space U+3000, ogham space U+1680), so a whitespace-only string was emptied before the scan ran — makingignoreWhitespace: falseunreachable for exactly those runes. The trim now only runs whenignoreWhitespaceistrue; when the caller opts in to classifying whitespace, the original string reaches the rune loop, so NBSP →Latin1Supplement, en-space →GeneralPunctuation, and ideographic space →CJKSymbolsAndPunctuationas the spec's Bulletproofing list requires. The defaultignoreWhitespace: truepath is unchanged. -
Corrected two
HebrewDateConvertertest expectations (hebrew_date_converter_test.dart) against SPEC-datetime-hebrew-converter.md. (1) The cross-cycle leap-boundary test asserted 5774→5777 as the last-leap/first-leap pair, but the closed-form(year*7+1)%19 < 7(used by the test's own far-future case and the source) places the seventh and final leap of a 19-year cycle at cycle-position 19, making 5776 the last leap and 5779 the first of the next cycle; 5774 is position 17 (the sixth leap). (2) The proleptic-day-before-epoch test (DateTime(-3760, 9, 6), JD 347997) expected month 13/day 29, but Hebrew year 0 is a degenerate proleptic year (its molad-derived length is 321 days, not a real 383–385-day leap year), so the month-walk lands on month 11, day 25 — the test now pins the algorithm's actual floor output. The source was correct in both cases; the test expectations were wrong.
Added #
Roadmap batch — 25 advanced, tree-shakeable utilities (each its own file + full test suite; 276 new tests):
FenwickTree(fenwick_tree_utils.dart, roadmap #483) — Binary Indexed Tree for O(log n) point updates and prefix/range sums overnum; 1-based internal tree behind a 0-based public API, withFenwickTree.fromListbulk build.MinMaxHeap<T>(min_max_heap_utils.dart, roadmap #499) — double-ended priority queue (min-max heap) giving O(1)min/maxpeek and O(log n)removeMin/removeMaxfrom a single backing array, withminOrNull/maxOrNullfor the empty case.SkipList<T>(skip_list_utils.dart, roadmap #502) — probabilistic ordered set withcontains/add/remove, ascendingvalues, andfloor/ceilingorder-statistic lookups; injectableRandomfor deterministic tests.clusterBySimilarity/dedupBySimilarity(similarity_dedup_utils.dart, roadmap #461) — single-link (transitive) near-duplicate clustering via union-find over a caller-supplied similarity predicate, preserving first-seen order.weightedSubset(constrained_subset_utils.dart, roadmap #476) — weighted random subset selection without replacement (Efraimidis–Spirakis) honoring exclusion sets and non-positive-weight skipping; injectableRandom.clusterIntoSessions/sessionsWithBounds(session_clustering_utils.dart, roadmap #485) — gap-based sessionization of timestamped items (clickstream/login/GPS bursts); sorts input and splits on gaps exceedingmaxGap, optionally returning each session's start/end bounds.BacktrackingSolver<State, Choice>(backtracking_utils.dart, roadmap #486) — generic depth-first backtracking with pruning callbacks and a solutionlimit;solveFirst/solveAll(verified on N-Queens and subset-sum).permutations/combinations/cartesianProduct/powerSet(lazy_combinatorics_utils.dart, roadmap #488) — lazysync*combinatorics that yield fresh lists on demand, so huge spaces are enumerable without materializing them.ItemSimilarityModel<T>(item_similarity_utils.dart, roadmap #490) — co-occurrence item-to-item recommender; Jaccard similarity over per-item basket sets with rankedrecommend(item, topN).multiSourceBfsDistances/multiSourceBfsNearest(multi_source_bfs_utils.dart, roadmap #535) — single-sweep multi-source BFS giving each node its hop distance to (and identity of) the nearest seed;-1sentinel for unreachable nodes.pageRank(pagerank_utils.dart, roadmap #541) — power-iteration PageRank with dangling-node mass redistribution and tolerance-based early stop; scores sum to ~1.enumeratePaths(path_enumeration_utils.dart, roadmap #543) — all simple (no repeated node) paths between two nodes with an optionalmaxDepthedge cap.reachabilitySets/reachabilityMatrix/canReach(reachability_utils.dart, roadmap #551) — transitive closure; a node reaches itself only when it lies on a cycle.cusumChangePoints(change_point_cusum_utils.dart, roadmap #568) — two-sided CUSUM change-point detection with configurable threshold/drift, resetting on each detection.giniCoefficient(gini_utils.dart, roadmap #573) — inequality metric in [0,1] via the sorted-rank formula;0for equal distributions, NaN for empty, asserts non-negative inputs.rollingCorrelation(rolling_correlation_utils.dart, roadmap #577) — Pearson correlation over a sliding window of two equal-length series; NaN where a window has zero variance.binByWidth/quantileBoundaries/binByBoundaries/binCounts(data_binning_utils.dart, roadmap #585) — equal-width, quantile, and custom-boundary binning with out-of-range clamping and frequency counts.billingDateInMonth/nextBillingDate/billingSchedule/currentCycle(billing_cycle_utils.dart, roadmap #609) — monthly billing anniversaries with end-of-month clamping (anchor 31 → Feb 28/29) and cycle-bounds resolution.RecurrenceSpec+RecurrenceFrequency+humanizeRecurrence(humanize_recurrence_utils.dart, roadmap #599) — human-readable recurrence text ("every 2 weeks on Tuesday", "every 2nd Tuesday of the month") with a correct English-ordinal helper.dailyCounts/heatmapGrid/heatmapStats(calendar_heatmap_utils.dart, roadmap #603) — GitHub-style heatmap data: per-day event counts, a week-aligned grid over a date range, and max/total/active-day stats.findGaps/fillMissing/forwardFill(timeseries_gap_utils.dart, roadmap #606) — detect missing samples in a roughly-regular series (tolerance-based), generate the complete regular grid, and forward-fill null values.parseCacheControl/parseMaxAge/parseETag/parseRetryAfterSeconds(http_header_parse_utils.dart, roadmap #632) — dependency-free parsing of common HTTP caching headers (directive map, weak/strong ETag, numeric Retry-After).stableHash/canonicalString(stable_hash_utils.dart, roadmap #649) — order-stable structural checksum (FNV-1a 64-bit, sorted map keys, order-preserving lists) rendered as a fixed 16-char lowercase hex; no crypto dependency.flattenMap/explode(flatten_explode_utils.dart, roadmap #648) — flatten nested maps/lists into dotted/indexed keys and explode an array field into one row per element for tabular/BI export.CancellationToken/CancellationException/runCancellable(cancellation_token_utils.dart, roadmap #674) — cooperative cancellation: idempotentcancel,throwIfCancelled,whenCancelledfuture, fire-onceonCancelcallbacks, and a task-vs-cancellation race helper.
Roadmap batch 2 — 10 more advanced, tree-shakeable utilities (each its own file + full test suite; 80 new tests):
-
SegmentTree(segment_tree_utils.dart, roadmap #495) — iterative segment tree for O(log n) range sum / min / max with O(log n) point updates;SegmentTree.sum/.min/.maxnamed constructors with identity-aware empty handling. -
BkTree(bk_tree_utils.dart, roadmap #493) — metric-tree index for approximate string matching:search(query, maxDistance)prunes via the triangle inequality (default Damerau–Levenshtein, injectable metric), set-semantics inserts. -
SpatialGrid<T>(spatial_grid_utils.dart, roadmap #506) — uniform-grid spatial index for 2D points; O(1)insertandqueryRadius(x, y, r)that scans only overlapping cells then filters by true Euclidean distance. -
TimeSeriesBuffer+TimeBucket(timeseries_buffer_utils.dart, roadmap #510) — bounded series that keeps the last N raw points verbatim and folds evicted points into fixed-width count/sum/min/max/mean buckets for raw-recent + aggregated-history views. -
WriteThroughStore<K, V>/WriteBackStore<K, V>(write_through_cache.dart, roadmap #508) — async cache wrappers that coordinate writes to a backing store (the read-throughWriteThroughCache#523 does not): write-through persists on everyput; write-back buffers dirty keys and coalesces them onflush. -
MruCache<K, V>(mru_cache.dart, roadmap #509) — fixed-capacity cache with most-recently-used eviction (the right policy for cyclic scans) plus per-key access-frequency tracking viafrequencyOf. -
simplifyDegree2Chains(graph_simplify_utils.dart, roadmap #545) — contracts maximal chains of degree-2 pass-through nodes in an undirected graph into single junction-to-junction edges; preserves anchorless pure cycles so connectivity is never lost. -
serializeAdjacency/parseAdjacency(graph_serialize_utils.dart, roadmap #554) — loss-free compact text codec (0>1,2;1;2>0) for directed adjacency-list graphs; nodart:convertdependency, preserves neighbor order and isolated nodes. -
diffCalendars+CalendarEvent(calendar_diff_utils.dart, roadmap #608) — classifies two event snapshots (keyed by id) into added / removed / changed, where "changed" compares start, end, and title; the core of sync reconciliation and "what changed" UIs. -
RateLimitSchedule(rate_limit_schedule_utils.dart, roadmap #612) — shapes requested event times to honor a rolling-window quota (max N per period) plus a minimum cooldown, pushing rather than dropping events to the earliest compliant instant. -
findUnicodeClassType(String)+UnicodeClassTypeenum +isUnicodeWhitespace(int)(unicode_class_utils.dart, unicode_class_type.dart) — script/block detector that inspects a string's runes and returns the Unicode named block (BasicLatin,Cyrillic,Arabic,CJKUnifiedIdeographs,Hiragana,HangulSyllables,CurrencySymbols,Arrows, ...) the first qualifying rune falls into, mirroring the .NET "Supported Named Blocks" table across the Basic Multilingual Plane (U+0000–U+FFFF). Useful for language/script detection, input validation, sort-bucket selection, and font-fallback decisions. Options:ignoreBasicLatinskips the Latin block so a Latin-prefixed string reports the trailing non-Latin script;firstCharOnly(defaulttrue) classifies only the first qualifying rune vs. scanning to the first non-ignored match;ignoreWhitespace(defaulttrue) skips whitespace runes. Returnsnullfor an empty/whitespace-only string, for unassigned gaps between blocks, and for astral-plane runes above U+FFFF (most emoji) — the function is BMP-only and the three surrogate enum blocks are unreachable from valid Dart strings. Never throws (best-effort contract; any failure degrades tonull). The companionisUnicodeWhitespace(int rune)is the pure-Dart whitespace predicate (ASCII controls plus the Unicode space separatorstrimdoes not always strip, e.g. narrow no-break space, ideographic space) replacing the originalquiver.isWhitespacedependency, and the backingunicodeClassRangestable is exposed for meta-tests asserting it stays sorted, non-overlapping, and one-entry-per-enum-value. Pure Dart, no Flutter or external dependencies. -
String?.compareStringFolded()/foldedCompare()/List<String>.sortedFolded()(string_folded_compare_extensions.dart) — diacritic-folding sort comparator. Folds Latin diacritics and ligatures (á→a,ß→ss,æ→ae) so accented names interfile with their base letter instead of sorting afterzby code unit; case-insensitive by default, with an optionalnaturalnumeric mode (img2beforeimg10), null grouping (nullsLast), and a deterministic raw-string tie-break so two distinct strings that fold to the same key ("Foo"vs"fóò") never compare equal — safe as aSplayTreeMapcomparator (a0compare silently drops a key). Composes the existingremoveDiacritics()andnaturalCompare(); pure Dart, no new dependencies. Latin-focused: non-Latin scripts (CJK, Cyrillic, ...) pass through and order by code point; NOT locale-aware ICU collation. -
DateTimeIntlDisplayExtensionsonDateTime(date_time_intl_display_extensions.dart) — locale-correct date/time display viaintlskeletons (yMMMd,MMMEd,jm,Hms, ...), the one opt-in module inlib/datetime/that pulls theintldependency (the rest stays intl-free by design). Unlike the fixed-layoutDateFormatNamespresets, these REORDER components per locale (en_US "Jan 15, 1945", fr_FR "15 janv. 1945", ja_JP "1945年1月15日") and auto-detect each locale's 12h/24h clock from thejmskeleton. Members:dateDisplay(abbreviated/full month, optional English ordinal, current-year suppression),makeDisplayDate(weekday + year,year > 0guard for placeholder dates),makeDisplayTime(locale clock with minute-eliding and a U+00A0 non-breaking clock so "8:31 PM" never wraps),utcTimeDisplay(fixed presets via the newUtcTimeDisplayEnum),fullDateDisplay,formatByLocale,toDateFormat(explicit pattern + millisecond suffix), andgetUtcOffset(pure-DartUTC+5/UTC±0/UTC+5:30). All display methods catch and degrade (null/'') rather than throw on an unloaded locale. The top-levelformatUtcOffset(Duration, {verbose})formats a fixed offset deterministically (no host-TZ read) for testability. Callers formatting non-English locales must callinitializeDateFormatting()once per process first. Requires theintlpackage (re-enabled inpubspec.yaml). -
String.removeEndNullable(String? find)(string_manipulation_extensions.dart) — nullable-aware companion toremoveEnd, sitting beside it onStringManipulationExtensions. Returns the receiver unchanged whenfindisnullor empty (nothing to strip); returnsnullonly for the empty-receiver-with-a-real-suffix case (an empty source cannot carry the requested suffix, sonullmarks "no source" distinctly from'"stripped to nothing"); otherwise delegates toremoveEnd, so a whole-string match strips to'(notnull) and only ONE trailing occurrence is removed. Matching is case-sensitive UTF-16 code-unit suffix matching (not grapheme-aware), so a surrogate-pair emoji or whole combining mark strips cleanly while a bare combining mark off a cluster can split it. Pure Dart, no Flutter or external dependencies. -
bool.compareTo(bool other)(bool_sort_extensions.dart) — adds theBoolSortingHelperextension so aboolcan be used as a sort key.booldoes not implementComparableindart:core, so[true, false].sort()throws; this fills the gap with aComparable.compareTo-shaped comparator (truesorts BEFOREfalse, the "flagged first" convention): returns0for equal values,-1when this istrueandotherisfalse, and1for the reverse. Drops intoitems.sort((a, b) => a.isPinned.compareTo(b.isPinned))or any tie-break comparator. Pure Dart, no Flutter or external dependencies; the closed two-value domain has no failure path. -
FilterValue<T>(filter_value.dart) — optional-write wrapper forcopyWithparameters that need three intents instead of two:FilterValue.unset()(the const default) keeps the current value,FilterValue(v)overrides withv, andFilterValue(null)explicitly clears to null — the case the?? this.fieldidiom can't express and that the*ForceNullcompanion-parameter workaround doubled the parameter surface to express.resolve(current)performs the keep-vs-override decision in one call; the wrapper holds its value by reference (no copy) and defines nooperator ==. Pure Dart, zero dependencies. -
DateTime.toAnnualDate/DateTime.toDayRange()(date_time_bounds_extensions.dart) — two members added toDateTimeBoundsExtensions.toAnnualDatepins month/day to sentinel year 0 (the canonical recurring-annual-date form for birthdays/anniversaries) — the blessed producer for the existingDateTimeComparisonExtensions.isAnnualDateInRangeconsumer, dropping time-of-day and surviving Feb 29 (year 0 is a leap year).toDayRange()returns the full local calendar day as aDateTimeRangeby composing the existingstartOfDay/endOfDaygetters, so the end is microsecond-precise (23:59:59.999999), not millisecond-truncated, with a single source of truth for the bounds. -
DateTimeUtils.monthDayCountSafe({int? year, required int month})(date_time_utils.dart) — null-year-tolerant, non-throwing companion tomonthDayCount. Whenyearis null, February returns 28 (a leap year cannot be resolved without the year); a known leap year still yields 29. An out-of-rangemonth(0,13,-1, very large ints) returns 30 instead of throwing, so callers holding partial date parts get a defined value. Negative years follow the proleptic Gregorian leap rule. Backed by newDateConstants.daysInThirtyOneDayMonth/daysInThirtyDayMonth. -
Duration.displayTime()/Duration.formatDuration()/Duration.reverse()(duration_clock_format_extensions.dart) — a newDurationClockFormatExtensionswith three pure-Dart formatters.displayTime()renders a stopwatch/media clock string ('HH:MM:SS.mmm', or'MM:SS.mmm'withshowHours: false) and never rolls hours into days, soDuration(hours: 25)shows'25:00:00.000'— distinct from the day-aware top-levelformatDuration(d, ...)it sits alongside.formatDuration()renders a comma-joined human list of non-zero units down to microseconds ('1 hr, 2 mins, 3 secs, 4 ms, 5 μs'), returning'Instantaneous'forDuration.zero, with optionalshowLeadingZerosand short/longshortFormwords (microsecond short form is the Greek muμs).reverse()returns the sign-negated duration. Reuses the library'sString.pluralize; no Flutter, nointl. -
Uint8List.toIntList()/List<int>.toUint8List()(uint8list_extensions.dart) — two reciprocal byte/integer bridges in new extensionsUint8ListExtensionandIntListExtension.toIntList()copies a fixed-lengthUint8Listinto a fresh growable, independentList<int>(the form binary APIs — file bytes, crypto output, network frames, image decoders — need when they must append or mutate).toUint8List()copies anyList<int>into a fresh fixed-lengthUint8ListviaUint8List.fromList, storing each element modulo 256 (low 8 bits):256 → 0,257 → 1,-1 → 255, and even platform-max/min ints reduce to their low byte without throwing — narrowing that holds for typed-data sources (Int8List/Int16List) too. Both directions produce independent buffers, so post-conversion mutation never crosses over. Pure Dart (dart:typed_dataonly), no Flutter, no external packages. -
StopRangeenum (gradient_stop_range.dart) — four named easing categories (easeIn,easeOut,easeInOut,linear) mapped via.stopsto a fresh normalized two-elementList<double>in the 0..1 range ([0, 0.5],[0.5, 1],[0.25, 0.75],[0, 1]). Drops straight into any stops-accepting gradient API (FlutterGradient.stops, a CSS/SVG string builder, a custom shader). Pure Dart, no Flutter or external dependencies; each read returns an independent list so caller mutation can't corrupt the constants. -
double.toAspectRatio()(double_aspect_ratio_extensions.dart) — converts a decimal ratio (e.g. an image'swidth / height) into a GCD-simplified integer pair(int, int)?, quantizing the fractional part to three decimal places (× 1000) and reducing viaIntUtils.findGreatestCommonDenominator. Pure Dart, no dependencies; reuses the package'shasDecimalsgetter and GCD utility. Whole numbers return(1, value); the tuple order is(denominator-side, value-side), locked by a regression test. Returnsnullfor NaN,±Infinity, and negative fractional input — the non-finite guard keeps the integral path from throwingUnsupportedErrorontoInt(). The 3-decimal quantization truncates toward zero and is intentionally lossy, so canonical small ratios such as16:9are not recovered (16 / 9→(1000, 1777)). -
DarkColorsenum +DarkColorsUtils.darkColorMap(dark_colors.dart) — 20-value enum (Red…Black) mapped to fixed, fully-opaque Material 700Colorconstants (e.g.Red → 0xFFD32F2F). A brand-agnostic palette of legible-on-light dark colors for category tags, avatar backgrounds, chart series, and labels. Compile-timeconstdata table; every swatch is distinct, alpha0xFF, and verified by tests to clear a 3.0 WCAG contrast floor against white (cross-checked via the package's owncontrastRatio). Enum order is index-stable (Red= 0,Black= 19) so values persisted by index never silently remap. Flutter-typed (needsColor); kept in its own file so the pure-Dartcolor_utils.dartmath stays Flutter-free. -
SimpleRelativeDayenum +DateTime.getSimpleRelativeDay()/getRelativeDayResult()(simple_relative_day_utils.dart) — calendar-day relative classifier that buckets a date against a referencenowintotoday/yesterday/tomorrow/beforeYesterday/afterTomorrow/lastWeekday/nextWeekday(±3..±13 days) /lastMonth/nextMonth, returningnullfor dates outside the ~2-month window (same month beyond ±2 weeks, or more than one calendar month away) so callers get a typed label only when one is useful. Covers BOTH past and future at single-day resolution — net-new alongside the past-only stringrelativeDateBucketand the free-textrelativeTimeString. Time-of-day is dropped (calendar days, not 24-hour spans) via an internal kind-preserving date-only normalize, so a 23:59-vs-00:01 pair reads as one day and a DST short day still counts whole days; the month delta folds the year ((year*12 + month)) so Dec→Jan reads as next month.getRelativeDayResulttakes a caller-suppliedweekdayFormatterfor the "Last/Next [Weekday]" label so the core staysintl-free and never touches app locale state — the formatter is invoked at most once and only for weekday buckets (a throwing formatter propagates rather than being swallowed). The four unreachableThisWeek/LastWeek/NextWeek/ThisMonthvalues from the source app were dropped at inclusion (the classifier never emitted them). Pure Dart, no Flutter, no external packages. -
num.formatNumber({format, locale})(num_intl_format_extensions.dart) — a newNumIntlFormatExtensionsthat renders anynumwith CLDR-accurate, locale-aware grouping/decimal separators by delegating tointl'sNumberFormat(format, locale).format(this).formatis an arbitrary ICU number pattern (default'#,##0'; e.g.'#,##0.00'two decimals,'##,##,##0'Indian 2-2-3 grouping,'#,##0;(#,##0)'parenthesized negatives,'#,##0%'which scales the receiver by 100,'00000'zero-pad).localeis a BCP-47 string ('de_DE'produces'1.234','es_ES'produces'1.234.567','fr_FR'groups with U+202F a NARROW no-break space, not U+00A0); when null it follows the process-globalIntl.defaultLocale. This is the CLDR-accurate counterpart to the dependency-freeformatNumberLocalefree function (which inserts hand-supplied separator strings with no locale data and cannot express non-3-digit grouping or decimal-comma locales) — both are kept, trading theintldependency against accuracy. Rounding is half-up (2.5produces'3');double.infinity/negativeInfinity/nanrender the locale symbols (infinity, minus-infinity, NaN),-0.0is preserved as'-0', and an unknown/emptylocalethrowsArgumentErrorrather than silently falling back. Opt-in module pulling the already-presentintldependency, mirroringdate_time_intl_display_extensions.dart. -
DayInMonthCalculations(month_weekday_named_extensions.dart) — named, readability-focused wrappers over the existingMonthWeekdayUtilscore for the exact phrasings DST and public-holiday rule tables are written in:firstMonday/secondMonday/thirdMonday,firstThursday/firstFriday/firstSaturday/firstSunday,secondFriday/secondSaturday/secondSunday,thirdSaturday/thirdSunday, pluslastMonday/lastThursday/lastFriday/lastSaturday/lastSunday. Every 1st/2nd/3rd/4th andlast*occurrence is non-null (a calendar invariant —thirdSaturday/thirdSundaycorrected from the over-cautious nullable original to matchthirdMonday); an out-of-rangemonththrows a namedStateErrorrather than returning a plausible-but-wrong neighboring-month date, and the genuinely-absent-able 5th-occurrence query stays onMonthWeekdayUtils.nthWeekdayOfMonth. AddsdaysInFebruary(year)(leap-aware via the day-0-of-March trick, correct for ÷4/÷100/÷400, year 0, and negative years), plusfirstDayOfMonth(year, month)andlastDay(year, month)month-boundary helpers (the latter handles the December month-13 roll-over). All results are local midnightDateTime. Pure Dart, no Flutter, no external packages. -
RelativeTimeUtils(date_time_relative_predicate_extensions.dart) —DateTimeextension with four net-new calendar-day predicates plus a descriptive relative-time formatter.isTomorrow/isYesterdaycompare date-only viaisSameDateOnly(time-of-day irrelevant, so 23:59 vs 00:01 of the adjacent day still matches);isOlderThanToday/isOlderThanYesterdayuse exclusive start-of-day instant boundaries viaisBefore(the exact midnight returnsfalse, one microsecond earliertrue). Each takes an optionalnowfor deterministic tests. The two families intentionally diverge on UTC-vs-local (field comparison vs instant comparison) and are documented as not-DST-safe across a transition (the fixed 24h add/subtract).relativeTime({now, isDescriptive, isDescriptiveTimeSuffix, roundUp})returns verbose ("a moment ago","about an hour from now","23 years ago") or terse ("now","~1h","2y") phrasing, with an optionalago/from nowsuffix and floor-vs-round numeric units. Year-level spans use date-based calendar arithmetic (subtract years, back off one if the anniversary has not yet passed) instead ofdays / 365.25, fixing the off-by-one near anniversary boundaries (a2000-12-31birthday reads23 yearson2024-06-15, not24). Pure Dart; depends only onisSameDateOnlyandString.pluralize. The existing top-levelrelativeTimeStringis untouched — this is a distinct, additive API with a different output dialect. -
Map<String, V>.sortMap()(map_initials_sort_extensions.dart) —InitialsSortingUtilsextension returning aSplayTreeMap<String, V>ordered "letters before numbers" (from SPEC-sort-utils.md): letter-initial keys sort alphabetically and come first, number-initial keys follow with PURE integers in numeric order so'10'sorts after'2'(not before, the lexicographic trap). Letter detection is ASCII-only ([a-zA-Z]), so accented/non-Latin/emoji/symbol/empty initials fall to aString.compareTolexicographic fallback — documented and tested. Fixes a latent correctness bug in the source: two distinct pure-int keys of equal value ('007'vs'7') madeint.compareToreturn0, which would silently DROP one entry from theSplayTreeMap; a lexicographic tie-break now retains every entry. Signed ('-5') andint-overflowing ('99999999999999999999') keys correctly fall through to lexicographic without throwing. Pure Dart (dart:collectiononly), no Flutter or external packages. -
sortNullableStringListInPlace(List<String?>)(list_nullable_string_sort_extensions.dart) — top-level helper that sorts aList<String?>in place, case-insensitively, withnulls grouped to the front, returningtrueon success (thefalsebranch is defensive — the comparison cannot throw forString?). Delegates to the library's existingcompareStringNullable(single source of truth for null-aware string ordering) rather than re-implementing the lowercase/null-coalescing, and wraps the comparison so callers avoid the extension-on-List<String?>lint at each call site. Comparison is lowercased UTF-16 code-unit order, NOT locale-aware:'é'(U+00E9) sorts after'z', andnull/''both collate as the empty string. Pure Dart, no Flutter or external packages. -
compareAges(DateTime? a, DateTime? b, {bool ascending = true})(date_time_compare_age_extensions.dart) — nulls-LAST nullableDateTimecomparator with a direction flag, the counterpart tocompareDateTimeNullable(which placesnullFIRST and has no direction option). Twonulls are equal (0); a singlenullsorts LAST in BOTH directions because the null branches return1/-1BEFORE theascendingmultiplier is applied (a regression that reordered them would float nulls to the top — pinned by tests). Non-null values compare by absolute instant (DateTime.compareTo), so UTC-vs-local of the same moment is equal, DST transitions and calendar boundaries are irrelevant to ordering, and a one-microsecond difference is not truncated. Pure Dart, no Flutter or external packages. -
MaterialShade/MaterialShadeName/MaterialShadeLevels(color/material_shade.dart) — Flutter-scoped type-safe model of the tenMaterialColorintensities (50–900).valuegives the swatch index integer (Colors.blue[shade.value]),onShadereturns the contrast-correct foreground (black on the50–400band, white on500–900) per Material accessibility convention so callers never recompute luminance, anddisplayName/displayNameAnnotatedgive UI labels ("Shade 500", with(Lightest)/(Middle)/(Darkest)on the band endpoints, plain ASCII space so persisted/compared labels stay stable).MaterialShadeLevelsexposes the canonicallightShades/darkShades/ combinedshadesconst lists plus a seededrandomShade({bool? isLightBackground, int? seed})that draws from the high-contrast band for the given background —isLightBackground: truedeliberately yields a DARK shade for a light background. The picker reuses the library ownIterable.randomElement(the app-internalRandomList.randomItemdependency was dropped). -
ComputeStreamTransformer<TInput, TOutput>(async/compute_stream_transformer.dart) — genericStreamTransformerBasethat runs each incoming event through Flutter'scompute()(a one-shot background isolate) and emits the result, keeping CPU-bound per-event work (model mapping, parsing, serialization, byte decoding) off the UI thread. A thin, type-parametric wrapper overstream.asyncMap((d) => compute(fn, d)):asyncMapawaits each computation before pulling the next event, so output order always equals input order even when a later payload computes faster.computeFunctionmust be a top-level/static function (acomputerequirement — capturing closures cannot be sent to an isolate) and its payload must be isolate-sendable. The optionalonErrorrecovers a compute failure into a fallback value (one fallback per failing event, no collapsing) instead of erroring the stream; a SOURCE-stream error is forwarded untouched because it is not a compute failure, and a throwingonErrorsurfaces its error rather than swallowing it. On webcompute()runs inline (no real isolate) but still produces correct results. Flutter-typed (needspackage:flutter/foundation.dart); the app-specific contact-conversion specializations from the source were dropped as proprietary. -
String.toColor/Color.toHex/Color.darken/Color.lighten/Color.readableOn(flutter/color_extensions.dart) -- FlutterColor<-> hex-string conversion plus HSL lightness adjustment and a WCAG-contrast convergence helper (from SPEC-color-utils.md).String.toColor()parses a 6-digitRRGGBB(forced opaque) or 8-digitAARRGGBBhex string --#optional, case-insensitive, whitespace-trimmed -- returningnullfor empty/wrong-length/non-hex input (a0xprefix, leading sign, 3-digit shorthand, full-width or Arabic-Indic digits all yieldnull; embedded#is stripped everywhere).Color.toHex({includeAlpha})formats as uppercase, zero-padded, fixed-width#AARRGGBB/#RRGGBB, clamping wide-gamut channels above1.0so the width never breaks.darken/lightenadjust HSL lightness preserving hue/saturation/alpha and return the color unchanged for out-of-range,NaN, or infinite amounts (lenient, no-throw contract).readableOn(background, {minRatio = 4.5, step = 0.04, maxSteps = 16})darkens (light bg) or lightens (dark bg) the foreground until it clears the WCAG target, capped bymaxStepsso an unreachable pair (mid-gray on mid-gray, identical fg/bg,step == 0) returns best-effort instead of spinning. Flutter-typed; kept in its ownlib/flutter/file so the pure-Dartniche/color_utils.dartint-channel math stays Flutter-free. -
TextDirectionParseUtils.tryParse+TextWritingDirectionenum (string/text_direction_parse_utils.dart) — pure-Dart parser for the universal CSS/HTML/Unicodeltr/rtldirection tokens, returning a package-localenum TextWritingDirection { ltr, rtl }(from SPEC-string-text-direction.md). The Flutter-typedTextDirectionform from the source app was deliberately NOT ported — returningdart:ui'sTextDirectionfor a one-line wrapper would pull Flutter into this otherwise Flutter-free package; callers map to the framework type at the UI boundary.tryParseis case-insensitive and strips only LEADING/TRAILING whitespace viaString.trim()— which also strips Unicode whitespace (U+00A0 NBSP, thin space, ideographic space) but NOT zero-width chars (U+200B, U+FEFF BOM) or the direction marks themselves (LRM U+200E, RLM U+200F), all of which therefore yieldnull. No substring or partial matching ('ltrx','l t r','ltr;'→ null) and no Unicode normalization/folding (full-width'ltr', accented'ĺtr', emoji → null). ASCII-only lowercasing makes the result locale-independent (no Turkish-I pitfall). Total function: returnsnull(never throws) for null, empty, whitespace-only, numeric strings, control characters, and arbitrarily long junk. Pure Dart, no Flutter, no external packages. -
List<String>.joinWithFinal/anyContains/removeTrimmedEmpty/firstNotEqualTo/toLowerCase/toUpperCase, plusList<String?>.toLowerCase/toUpperCase/removeNullsAndTrimmedEmpty(list/list_string_extensions.dart) — seven members added to the existingListStringExtensionsplus a newNullableListStringExtensions on List<String?>(from SPEC-string-list-helpers.md).joinWithFinal({separator, finalSeparator})joins a list into a British-style sentence with a distinct final connector (['a','b','c']->'a, b and c') — distinct fromjoinDisplayList's Oxford-comma form: it adds no comma before the connector and does no trim/dedupe, so a blank entry survives (['a','','c']->'a, and c'); returnsnullfor empty and the sole item for length 1.anyContains(check, {caseSensitive})is true when any element containscheckas a substring (null/emptycheckor empty list ->false; case-insensitive lowercases both sides once).removeTrimmedEmpty({trim})drops entries empty after trimming and returnsnull(never[]) so a caller distinguishes "no items" from a result; withtrim: falseonly literally empty strings are dropped (a' 'survives).firstNotEqualTo(value)returns the first element not equal tovalue(case-sensitive), or the first element whenvalueis null, elsenull.toLowerCase/toUpperCaseare invariant-culture element-wise mappings (Turkish'i'->'I', German'ß'->'SS', emoji round-trip unchanged). The nullable variants drop nulls via core-DartnonNullsfirst. Pure Dart; reusespackage:collection(firstOrNull/firstWhereOrNull) and the library's ownString.nullIfEmpty(trimFirst:)/List.nullIfEmpty(). -
ColorUtils(flutter/material_color_utils.dart) — three Flutter Material palette helpers with no app-domain knowledge (from SPEC-color-material.md).materialColorsis the canonical ordered, duplicate-free, immutableconstlist of the 19 primaryMaterialColorswatches (Colors.red…Colors.blueGrey) for indexing a fixed palette (charts, avatars, tags, deterministic per-index colors).getWhiteContrastColor(int)maps ANY int — negatives, values past 99, andintmin/max — deterministically to one of 100 fully-opaque colors biased to contrast against white, normalizing via((n % 100) + 100) % 100so the0–9palette indices stay in range on the VM and on web (JS-double ints) alike, then alpha-blending a tens-digit primary over a ones-digit secondary; every result is alpha1.0and clears a measured WCAG contrast floor against white (lightest blend ≈1.22).getColor(MaterialShade, MaterialColor)is a typed, exhaustive-switch swatch accessor replacing the stringlycolor[500]!. Reuses the sharedMaterialShadefromlib/color/material_shade.dartrather than redefining it. Flutter-typed (needsColor/MaterialColor); no external packages. -
HebrewDateConverter(datetime/hebrew_date_converter.dart) — net-new Gregorian→Hebrew (Jewish) lunisolar calendar converter and formatter (from SPEC-datetime-hebrew-converter.md), the library's first non-Gregorian calendar system. Implements the fixed arithmetic Hebrew calendar from "Calendrical Calculations" (Reingold & Dershowitz) via a Julian-Day-Number bridge: deterministic, offline, nointl, no I/O.fromGregorian(DateTime)returns a plain({int year, int month, int day})record (Hebrew year 57xx, month 1–13 in display order, day 1–30);isHebrewLeapYear/monthsInHebrewYear(12 or 13) /daysInHebrewYear(353–355 regular, 383–385 leap) /daysInHebrewMonth(29 or 30, with variable-length Cheshvan/Kislev and the four Rosh-Hashanah postponement rules) expose the calendar arithmetic.getMonthName(month, year, {useHebrew})resolves the leap-year Adar I / Adar II split (and plain Adar in a regular year) in English transliteration or Hebrew script;formatDayHebrew/formatYearHebrewrender gematria numerals (with the 15→ט״ו / 16→ט״ז substitutions that avoid spelling a divine name, and the thousands digit elided for years);format/formatDayMonthcompose the full strings ('1 Tishrei 5785'or'א׳ תִּשְׁרֵי תשפ״ה'). Civil-date mapping by design: only the input's year/month/day are read — time-of-day, UTC/local flag, and sunset are ignored, so an eveningDateTimeis NOT advanced to the next Hebrew day.abstract finalnamespace, all static; pure Dart, onlypackage:meta(@useResult).
Maintenance
Tests
- Full
String.removeEndNullablecoverage (string_manipulation_extensions_test.dart) implementing the sample cases and every bulletproofing gap from SPEC-string-misc-helpers.md: the null/empty-find no-op branches; the empty-receiver null-vs-empty split across {null, empty, non-empty} find; whole-string match to'(not null); suffix-longer-than-receiver and find-contains-receiver-plus-more no-match; single trailing occurrence of a repeated suffix; case-sensitivity; multi-byte accented-letter and surrogate-pair emoji suffixes (asserting no lone surrogate remains); a bare combining-mark strip documenting UTF-16 (non-grapheme) matching; and ASCII + non-breaking-space whitespace suffixes. - Code-point guard coverage for the
StringExtensionstypographic constants (string_special_chars_test.dart) implementing the sample value-assertion tests and every bulletproofing gap from SPEC-string-special-chars.md: exact code point per constant (smart quotes, non-breaking space/hyphen, Unicode/soft hyphen, zero-width space, Hangul-fillerblank, ellipsis, double chevron, apostrophe, bullet); single-rune guarantee for every glyph (plus an explicit LF-not-CRLF check onnewLine); UTF-8 encode/decode round-trip for all 18 members (guarding a re-encode flattening a smart quote to ASCII or losing the zero-width space); distinctness (nonBreakingSpace!= regular space,zeroWidthnon-empty,blanksurvivingtrim()); alias integrity (dot == bullet,lineBreak == newLine,apostrophe == accentedQuoteClosingboth U+2019);dotJoinerexact space+bullet+space shape; and const-evaluability proven by aconstlist fixture plus thecommonWordEndingsconst-list embedding. No source change — the constants already ship instring_extensions.dart. - Full coverage for the Flutter
Colorextensions (color_to_color_test.dart, color_to_hex_test.dart, color_light_test.dart, color_readable_test.dart) implementing the sample cases and every bulletproofing gap from SPEC-color-utils.md:toColorwhitespace/tab/newline/non-breaking-space, internal whitespace, multiple/embedded#,0x-prefix, leading sign, full-width and Arabic-Indic digits, 3-digit shorthand, wrong-length, all-zero/all-F, and null-safe call sites;toHexzero-padding, half-channel and 0.5 rounding boundaries, wide-gamut>1.0clamp, uppercase and fixed-width assertions, and full round-trips;darken/lightenNaN/+/-infinity/out-of-range no-op, epsilon near-no-op, pure black/white floors, grayscale no-NaN, transparent-alpha preservation, and the clamp-breaks-true-inverse boundary;readableOnunreachable best-effort (minRatio21, identical fg/bg), trivially-satisfiedminRatio <= 1,maxSteps0/negative,step0/NaN/negative termination, the 0.45 luminance split direction, and foreground-alpha preservation. - Full coverage for
ComputeStreamTransformer(compute_stream_transformer_test.dart) implementing every sample case plus every item in the SPEC-stream-compute-transformer.md bulletproofing list: in-order mapping, single event, empty source, order preservation under varied latency, nullTInput/TOutputround-trip, rethrow-on-error withoutonError(asserting pre-error values still emit),onErrorfallback (including when the FIRST event throws and for consecutive errors with one fallback each), a throwingonErrorsurfacing on the stream, a 100k-element large payload, Unicode/non-breaking-space/ellipsis/emoji string integrity, numeric extremes (0, negative,±Infinity,NaNviaisNaN,double.maxFinite,intnear 2^53), source-stream error passing through untouched despiteonError, broadcast-source no-replay behavior, cancellation mid-stream emitting nothing further, andonDonefiring after an error. - Hardened
MapExtensions.countItemscoverage (map_extensions_test.dart) with the bulletproofing edge cases from SPEC-map-count-items.md: all-empty values, single key/element, order-independence (empty value first vs last),Setpost-dedup count, lazy generated/mapped iterables, a1 << 20huge value and 1000-key sums (64-bit no-overflow), enum and custom-object keys (with equal-key collapse), Unicode/emoji and content-irrelevant elements, nullable elements counting toward length, fold-associativity (one-huge vs many-small), and an immutability snapshot assertion. - Full coverage for
DayInMonthCalculations(month_weekday_named_extensions_test.dart) implementing every case in SPEC-datetime-day-in-month.md: real US/EU/GB/Egypt DST anchor dates;daysInFebruaryacross standard/common/century-non-leap/century-leap/year-0/negative years;lastDayfor every 31-/30-day month, leap/common February, and the December month-13 roll-over; the 5th-occurrence present-vs-absent matrix for all seven weekdays; a non-null property test asserting 1st–4th exist for all weekdays across all 12 months of several years;StateErroron out-of-range months; local-midnight (isUtc == false, time-of-day zero) assertions across a DST fold; and far-future/far-past extremes (year 9999 and year 1). - Full coverage for
SimpleRelativeDay(simple_relative_day_utils_test.dart) implementing every case from SPEC-datetime-simple-relative-day.md plus its bulletproofing gaps: every exact-bucket boundary on both sides (±2/±3, ±6/±7, ±13/±14); the null-window vsmonthDiffinteraction (same-month +14..+20 → null, next-month +14 → nextMonth, large day-delta still nextMonth, ±2 months → null, Dec→Jan and Jan→Dec cross-year rollovers); leap Feb 29 in both directions, Jan 31→Mar 1 (monthDiff 2 → null), Mar 31→Feb 28; time-of-day dropped at midnight and one-microsecond boundaries; DST spring-forward and same-kind UTC inputs; Dart DateTime extremes (year 1 and 275760-09-13, no(year*12)overflow); default-nownon-throwing; a ±400-day sweep asserting only supported buckets are ever returned; and the weekday-formatter contract (verbatim forwarding, never invoked for exact/month buckets, null when absent, throwing-formatter propagation). - Full coverage for
NumIntlFormatExtensions.formatNumber(num_intl_format_extensions_test.dart) implementing every case in SPEC-num-locale-format.md plus its bulletproofing gaps: default/explicit en_US grouping; de_DE/es_ES dot grouping; the fr_FR U+202F narrow no-break group separator (built from the exact code point, never a literal space); de_DE decimal comma; en_IN 2-2-3 grouping; Arabic Western-digit grouping;ArgumentErrorfor unknown ('xx_YY') and empty ('') locales; pattern overrides (two-decimal, empty-string no-grouping,'00000'zero-pad, literal' kg'text,'#,##0%'×100 scaling); zero/small/grouping-boundary values (999/1000/99999); negatives (sign placement, parenthesized-negative, preserved-0.0); half-up rounding (1234.567/1234.565and2.5/3.5); int-vs-double receiver parity; extremes (9223372036854775807,double.maxFinitewith no exponent leak,0.0001at four decimals,±infinityandNaNlocale symbols viaString.fromCharCode(0x221E)); andIntl.defaultLocaledeterminism with per-test setUp/tearDown reset. - Full coverage for
ColorUtils(material_color_utils_test.dart) implementing the sample cases plus every bulletproofing gap from SPEC-color-material.md:materialColors19-in-order/no-duplicates/const-immutability (UnsupportedErroron.add);getColorexact swatch entry per shade, shade500 == base color, the full swatch×shade matrix assertingswatch[shade.value], and a partial customMaterialColorthrowingTypeErroron a missing level;getWhiteContrastColordeterminism, negative→non-negative-modulo equivalence (-1≡99), modulo-100 wrap (100≡0,142≡42), full opacity for all0..99,intmin/max no-throw + opacity, the exact red-on-red blend for0, boundary inputs9/10/90/99opacity and9≠10distinctness, and a WCAGcontrastRatio-against-white floor (≥1.15) over all0..99cross-checked via the package's ownniche/color_utils.dart. - Full coverage for the
List<String>/List<String?>string helpers (list_string_extensions_test.dart) implementing every sample case and every bulletproofing gap from SPEC-string-list-helpers.md:joinWithFinalempty/single/two/three-item connectors, no-Oxford-comma, custom/empty/multi-char separators, blank-entry-survives divergence, separator-bearing element not re-split, 10k-element no-stack join, and non-mutation;anyContainscase-sensitive/insensitive substring matching, null/emptycheck, empty list, whole-element and over-longcheck, accented-Unicode insensitive match, zero-width-space needle, and 10k-element short-circuit;removeTrimmedEmptyall-blank->null, trim-survivors,trim:falsekeeps whitespace-only/drops only literal empty, single empty/space, the non-breaking-space-dropped vs zero-width-space-survives boundary, and non-mutation;removeNullsAndTrimmedEmptythe ported all-null/null-removal/trim cases plus single-null, mixed-null,trim:falsesurvivor, and non-mutation;firstNotEqualTonull-value, empty list, differing element, all-equal, case-sensitivity, empty-string value, and single-equal->null; andtoLowerCase/toUpperCaseelement-wise mapping, nullable null-dropping, empty/all-null lists, German eszett length change, invariant-culture (not Turkish) casing, accented round-trip, emoji round-trip, and non-mutation. - Lint-cleanup refactors with no coverage change: the
unicodeClassRangesone-entry-per-enum assertion (unicode_class_utils_test.dart) useshasLength(1)instead of.lengthcompared to the literal1(clearer failure output, satisfiesavoid_misused_test_matchers); the lighten suite'sColorLightExtensions(color_light_test.dart) hoists the sharedoriginalbaseline into asetUpso each test recomputes it without leaking state.
1.5.1 - 2026-06-11 #
Exact length (meters ↔ feet) and weight (kilograms ↔ pounds) unit conversions with full-precision factors, plus English string formatters that handle the 12-inch carry boundary, negative values, and non-finite input. log
Added #
LengthConversionUtils/WeightConversionUtilsinlib/num/unit_conversion_utils.dart: dependency-freeconvert*primitives (NaN/infinity propagate) and*ToStringformatters.feetToStringcarries rounded inches into feet so it never renders "12 inches", signs negative heights, and falls back safely on NaN/∞.String.preventOrphans({int minWrapChars = 4})(string_wrap_extensions.dart) — typography helper that swaps a breaking space for a non-breaking space (U+00A0) wherever a wrap there would strand a token shorter thanminWrapChars(a lone…,I,(5),the). Symmetric rule (fuses when EITHER adjacent token is short, anywhere in the line), idempotent, pure-Dart with no dependencies. Splits on a single ASCII space only; code-unit length, not grapheme width.
Maintenance
Documentation
- Added inline WHY comments inside
LruLfuCacheUtils._evict(lru_lfu_cache_utils.dart) for the scan, tie-break, and empty-map guard — resolves the publish-audit sparse-comments flag.
Tests
- Filled the migrated-util coverage gaps harvested from Saropa Contacts (TEST-COVERAGE-migrated-utils-from-contacts.md): added a
weekOfYeargroup (previously untested — onlyweekNumber/numOfWeekswere covered) including the documented week-0 boundary; precomposed- and decomposed-accent cases forString.removeLastChars(the decomposed case documents that it actually trims by grapheme cluster viasubstringSafe, not by code unit); four/five-element Oxford-comma cases forList<String>.joinDisplayList; anddebounceAfterFirstlate-consumer regression + upstream-cancel-on-consumer-cancel cases forStreamDebounceExtensions.MonthUtils/WeekdayUtilswere already fully covered, and the harvested nullable-elementjoinDisplayListcases do not apply (the extension is onList<String>).
1.5.0 #
Ten advanced algorithm utilities from the roadmap-to-700 batch — quote-aware text folding, language detection, TF-IDF keyphrases, line-based redline diffs, Gale–Shapley stable matching, hierarchical clustering, HyperLogLog distinct counts, Pareto frontiers, time-decayed counters, and an LRU/LFU hybrid cache — each its own tree-shakeable file so apps pull in only what they use. log
Added #
foldText/unfoldText/FoldOptions(text_fold_utils.dart) — quote-aware hard-wrap and unwrap for email-reply text:foldTextwraps each line toFoldOptions.widthwhile repeating the leading quote prefix (>,> >) on every continuation;unfoldTextmerges soft-wrapped continuations back into one logical line per quote level. Words longer than the available width are emitted whole (never split — no infinite loop), blank lines are paragraph boundaries, and quote-depth changes are join boundaries. Roadmap #412.detectLanguage/LanguageGuess(language_detect_utils.dart) — best-effort dominant-language detection over a short string using character-trigram out-of-place rank distance against six compact built-in profiles (en/es/fr/de/it/pt). Returns the closest match with a0..1confidence, ornullwhen the text is too short (fewer than 3 trigrams) to decide. Heuristic, no data files, Unicode-safe. Roadmap #428.extractKeyphrases/Keyphrase/KeyphraseOptions(keyphrase_utils.dart) — TF-IDF keyphrase extraction over a single document against a corpus, returning the top-K ranked phrases (optional bigrams). Smoothed IDF guards the single-doc div-by-zero case; built-in English stopword filtering; deterministic tie-breaking. (termFrequenciesis also defined here but hidden from the barrel — the establishedtext_similarity_utilsexport keeps that name; import the file directly to use this one.) Roadmap #432.redlineDiff/RedlineEntry/RedlineOp(redline_utils.dart) — line-based track-changes diff: an LCS alignment classifies each line as unchanged / added / removed / changed (adjacent remove+add runs paired intochanged), each entry carrying its old and new line numbers. Immutable result, built for diff UIs. Roadmap #433.stableMatching/StableMatchingPrefs(stable_matching_utils.dart) — proposer-optimal Gale–Shapley stable matching from two-sided strict preference lists. Handles unequal set sizes and incomplete lists (unranked parties stay unmatched), and throwsArgumentErroron malformed input. Roadmap #448.hierarchicalCluster/cutClustersByCount/cutClustersByDistance/ClusterLinkage(hierarchical_cluster_utils.dart) — agglomerative hierarchical clustering on numeric vectors with single / complete / average linkage (Lance–Williams updates), producing a dendrogram you can cut at K clusters or a distance threshold. Default Euclidean distance, pluggable viaVectorDistance. Small-N intent (naive O(n³)). Roadmap #450.HyperLogLogUtils(hyperloglog_utils.dart) — HyperLogLog-lite approximate distinct count: configurable precision (2^pregisters), register-of-leading-zeros over a SplitMix64-mixed hash, harmonic-mean estimator with linear-counting small-range correction, and a pure register-wisemerge. Roadmap #454.paretoFrontier/ParetoOptions/ParetoDirection(pareto_frontier_utils.dart) — Pareto frontier (dominance filtering) over 2–3 objectives with per-dimension minimize/maximize directions; returns the non-dominated set in original order, retaining duplicates and all-equal points. Roadmap #463.TimeDecayCounter(time_decay_counter_utils.dart) — exponential time-decayed counter with O(1) memory via lazy decay (stored value + last-updated millis), a configurable half-life, and explicit injected timestamps (no wall-clock reads, fully deterministic). Defends against out-of-order timestamps. Roadmap #479.LruLfuCacheUtils(lru_lfu_cache_utils.dart) — bounded cache with hybrid eviction: lowest frequency first, least-recently-used as the tie-breaker.get/put/remove/length, with documented behavior for capacity 0/1 and key updates. Roadmap #480.
1.4.1 #
Small ergonomic utilities pulled from the Saropa Contacts adoption review — trailing-character trim, an integer-string predicate, and a natural-language Oxford-comma list join — so the app can delete its hand-rolled local copies. log
Added #
String.removeLastChars(int count)(string_manipulation_extensions.dart) — drop the last N characters, bounds-safe: a zero/negativecountis a no-op and acountat or abovelengthreturns''rather than throwing — so consumers stop hand-rolling thesubstring(0, length - n)guard (Saropa Contacts carried exactly this instring_utils_local.dart). Generalizes the existing single-characterremoveLastChar. Counts UTF-16 code units likeString.length, not graphemes (documented). ENH-001.String.isNumber(string_number_extensions.dart) — a getter that istruewhen the string parses as an integer viaint.tryParse, so consumers stop writingint.tryParse(s) != nullinline. Stricter than the existingisNumeric()(which parses asdouble): a decimal ('4.2') or scientific-notation ('1e3') value isfalsehere buttrueforisNumeric(). Inheritsint.tryParse's rules — accepts a leading sign and0x/0Xhex prefix, trims surrounding whitespace, and returnsfalsefor a magnitude that overflows the native 64-bitint. ENH-002.List<String>.joinDisplayList(...)(list_string_extensions.dart) — join strings into the natural-language form ('Alice','Alice and Bob','Alice, Bob, and Carol') instead ofjoin()'s barea, b, c. Trims each entry and drops blank/whitespace-only ones, de-duplicates by default (isUnique, via the existingtoUnique), and returnsnull— not''— for an effectively-empty input so callers can distinguish "no items". The three joiners (joiner/doubleJoiner/lastJoiner, the last defaulting to the Oxford comma', and ') are named params for locale/style control. ReusestoUnique+takeSafe. ENH-003.Iterable<T>.randomElement({int? seed})(iterable_extensions.dart) — the existing no-argrandomElement()gains an optionalseed, so the pick can be made deterministic (same seed + same iterable → same element) for reproducible demo data and stable widget tests. Routes through the library's ownCommonRandomRNG, replacing the baredart:mathRandom(). Default (no seed) behavior is unchanged — still a fresh pick each run. Picks byelementAt, so the iterable is not materialized to a list. ENH-004.String?.compareStringNullable(...)(string_compare_extensions.dart) — a null-tolerant string comparator, the counterpart to the existingcompareDateTimeNullableforDateTime?. Sorting aList<String?>(or objects by a nullable string field) no longer needs a hand-rolled(a ?? '').compareTo(b ?? ''). Mirrors the DateTime convention —nullsorts before non-null by default — with acaseSensitiveflag (default case-insensitive) and anullsLastflag to pushnulls to the end. Comparison isString.compareTo(UTF-16 code unit, not locale collation; documented). New file, exported from the barrel. ENH-005.StreamDebounceExtensions<T> on Stream<T>—debounce/debounceDistinct/debounceAfterFirst(stream_debounce_utils.dart) — the existing top-leveldebounceStreamfunction gains a chainable extension form so reactive pipelines read asstream.debounce(d).map(...). Plus two variants a database-watch UI needs:debounceDistinct(d, {equals})debounces and suppresses unchanged values, anddebounceAfterFirst(d)emits the first value instantly then debounces the tail — the correct shape for a.watch()stream (Drift, IsarfireImmediately: true) where the initial render must be instant but bulk writes should coalesce.debounceStreamis unchanged (back-compat); its deferred-listen single-subscription controller — which guarantees a late subscriber still receives the first emission (a fixed perpetual-spinner bug) — is now shared by all variants and has a dedicated regression test. ENH-006.MonthWeekdayUtils.nthWeekdayOfMonth/lastWeekdayOfMonth(month_weekday_utils.dart) — static(year, month)-keyed weekday-occurrence helpers, the no-seed counterpart to the existingDateTime.getNthWeekdayOfMonthInYearinstance extension. Calendar-construction code (DST rules, holiday tables) computes occurrences for an arbitrary year/month with no seedDateTimeto hang the instance call on; these take the year/month explicitly.nthWeekdayOfMonth(year, month, n, weekday)returns the nth weekday ornullwhen it does not exist (a 5th Friday in a 4-Friday month), guarding out-of-rangen/month/weekdayso an invalid argument can't compute a neighboring month;lastWeekdayOfMonth(year, month, weekday)("last Sunday of October") has no prior equivalent — the extension only counts from the start. Reuses the instance algorithm for the nth case. New file, exported from the barrel. ENH-007.parseCsv+CsvParseResult/CsvRowError(csv_parse_utils.dart) — an error-accumulating multi-line CSV parser layered over the existing single-lineparseCsvLine, for user-facing imports that must report every bad row rather than abort on the first. Splits on\n(strips trailing\r, skips blank lines), and sends a row toresult.errors(with 1-based line number, raw line, and reason) instead ofresult.rowson an unterminated quote (odd"count) or a column-count mismatch — the expected width coming fromexpectedColumnsor, whenhasHeaderis true, the header row. ENH-008 (part 1).retryWithPolicygainsretryIf(retry_policy_utils.dart) — abool Function(Object error)? retryIfpredicate so only transient failures are retried; returningfalserethrows immediately without consuming the remaining attempts, the delay, or firingonRetry(which already existed).retryWithJitteris intentionally untouched. ENH-008 (part 2).partialRatio/tokenSortRatio/tokenSetRatio(fuzzy_search_utils.dart) — fuzzywuzzy-style standalone string-similarity scores in[0, 1], so a consumer doing single-best-match selection with its own threshold can compute these directly and drop the externalfuzzywuzzydependency. (The per-resultscoreonfuzzySearchwas already public — this adds the missing ratio variants.)partialRatioslides a window for the best substring alignment ('New York'vs'New York City'→ 1.0);tokenSortRatiosorts tokens first for order-insensitivity;tokenSetRatiocompares the shared token core against each side's remainder (forgiving of extra words). All case-insensitive, reusingLevenshteinUtils.ratio. ENH-008 (part 3).
Maintenance
Tests
CsvRowErrordirect-construction coverage (csv_parse_utils_test.dart) — adds aCsvRowErrortest group asserting all three fields (lineNumber,line,message) andtoString(). The priorparseCsvtests only readlineNumber/messageoff returned errors, leaving thelinefield andtoString()unexercised; this closes that gap.
Tooling
- Publish audit: credit value-class constructors by field reads (audit.py) — the "untested public methods" check matched the constructor's name token in
test/, so a result/data class only ever built inside library code and returned (e.g.CsvRowError, produced byparseCsv) was flagged untested even when every field was asserted off the returned instance. The check now credits a constructor as covered when all its declaredfinalinstance fields are referenced in tests, and still flags the real gap when a field is never read. Internal tooling only — not shipped to package consumers.
1.4.0 #
Eleven more utilities from the Roadmap-to-700 set — INI/.env config parsing, RFC 5545 recurrence (parse + expand), a holiday-aware business-day calendar and SLA clock, ISO 8601 interval parsing, an interval-overlap tree, a dependency resolver, a priority task scheduler, a token-bucket rate limiter, and a resource pool — plus the capabilities catalog rebuilt from the Dart AST so every public symbol is listed. log
Added #
parseIni/parseEnv(ini_parser_utils.dart) — read the two near-universal flat-config formats with no dependency.parseInireturnssection → key → value(pre-header entries under the''global section, declared-empty sections preserved);parseEnvreturns a flatkey → valuemap for dotenv files and strips theexportprefix. The first=is the separator (sourl=http://host:80keeps its colon),#/;full-line comments and blanks are skipped, surrounding quotes are stripped (double-quoted values expand\n \t \r \\ \", single-quoted are literal), and#stays literal in values (no inline-comment stripping) so passwords/URLs/hex colors survive. A non-comment, non-section line lacking=throwsFormatException— strict so config typos surface. Roadmap #626.parseRrule+RecurrenceRule/RecurFrequency/RecurWeekday(rrule_parse_utils.dart) — parse a practical subset of the RFC 5545 recurrence rule used by iCalendar / Google Calendar exports (FREQ=WEEKLY;INTERVAL=2;BYDAY=MO,WE,FR;COUNT=10) into an immutable, value-equalRecurrenceRule. SupportsFREQ(DAILY/WEEKLY/MONTHLY/YEARLY),INTERVAL,COUNT,UNTIL(yyyyMMdd[THHmmss][Z], UTC vs floating),BYDAY,BYMONTHDAY(1..31 / -1..-31),BYMONTH, andWKST; an optionalRRULE:prefix is tolerated. Part order is irrelevant and duplicates take the last value. Unsupported parts (e.g.BYSETPOS,BYHOUR) throwFormatExceptionrather than being silently dropped, so the subset boundary is explicit.RecurWeekdaycarries itsDateTime.weekdaynumber for the companion iterator. Roadmap #591.expandRecurrence(recurrence_iterator_utils.dart) — the companion toparseRrule: lazily generate the concrete occurrences of aRecurrenceRulefrom a start instant, in ascending order. Async*generator, so an unbounded rule is safe — bound it with the rule'scount/until, thelimitargument, or.take(n). Walks one FREQ×INTERVAL period at a time and expands the BY rules within it: WEEKLY BYDAY positioned relative to WKST, MONTHLY/YEARLY BYMONTHDAY (negative = from month-end) with non-existent days (e.g. day-31 in a 30-day month, Feb-29 off leap years) dropped, BYMONTH filtering. The start supplies each occurrence's time-of-day and UTC-ness and acts as DTSTART (earlier candidates skipped); date math uses calendar fields, notDuration, so it never drifts across DST. Roadmap #592.BusinessCalendar(business_calendar_utils.dart) — a holiday-aware, weekend-configurable working-day calendar, extending the weekend-only free functions indate_time_business_days_utils.dart. Construct with a holiday list (matched by calendar day, time/zone ignored) and an optional custom weekend set ({DateTime.friday, DateTime.saturday}for regions that rest Fri/Sat); reuse it forisBusinessDay/isWeekend/isHoliday,nextBusinessDay/previousBusinessDay,addBusinessDays(skips weekends + holidays, negative goes backward),businessDaysBetween(count,[start, end)), andbusinessDaysIn(the list). Immutable after construction; calendar-field date stepping avoids DST drift. Roadmap #593.TaskScheduler(task_scheduler_utils.dart) — runs async tasks under a fixed concurrency cap, dispatching the highest-priority waiter whenever a slot frees (FIFO among equal priorities). The piece the FIFOAsyncSemaphoreUtilscan't offer: a semaphore admits waiters in arrival order, whereas this reorders the backlog by importance so an urgent task jumps ahead of queued background work (without preempting jobs already running).schedule(task, {priority})returns a future for the task's result; a failed task never stalls the scheduler (the slot is always released). Exposesrunning/pendingfor monitoring; a sub-1concurrencyis rejected. Roadmap #655.TokenBucketRateLimiter(rate_limiter_utils.dart) — smooths bursts to a sustainable average rate: tokens refill continuously attokensPerSecondup to acapacity(the burst ceiling, starts full), and work spends them.tryAcquire([tokens])is the non-blocking allow/deny primitive (no partial spend);timeUntilAvailable([tokens])reports the wait until a denied request would succeed, so the caller chooses to drop, queue, or delay;availableTokens()exposes the current fractional count. Time is read through an injectablenowclosure (defaults toDateTime.now), so refill is fully deterministic under test with noTimeror wall-clock coupling; a backward clock step accrues nothing. Requesting more thancapacity(or fewer than 1) throwsArgumentError. Roadmap #670.ResourcePool<T>(resource_pool_utils.dart) — a bounded, reusing pool for expensive-to-create resources (DB connections, HTTP clients, isolates). Grows lazily up tomaxSize, reuses idle resources, and when all are busy makes further borrowers wait FIFO until arelease.use(action)is the leak-proof entry point (acquire → run → release, even on throw); lower-levelacquire/releaseare also exposed, plusidleCount/inUseCount/waitingCount. A failedcreaterolls back the slot so a transient factory error doesn't permanently shrink the pool.close()disposes idle resources via the optionalonDispose, fails waiting borrowers withStateError, and blocks new acquisitions; resources still checked out at close are the caller's to dispose. Roadmap #666.parseIsoInterval(iso_interval_parse_utils.dart) — parse an ISO 8601 time interval into aDateTimeRange, supporting all three forms:start/end,start/duration, andduration/end(separated by/). Timestamps use Dart's native ISO parsing; the duration half is an ISO 8601 duration (P[nY][nM][nW][nD][T[nH][nM][nS]], weeks folded into days, fractional time components allowed) applied with calendar arithmetic so years/months add as calendar units (Jan 31 + P1M normalizes correctly) and time-of-day + UTC-ness are preserved. A missing separator, a malformed half, two durations, or an inverted (startafterend) interval throwsFormatException. Roadmap #646.IntervalTree<T>+IntervalEntry<T>(interval_tree_utils.dart) — an overlap (stabbing) query index over inclusive[low, high]intervals:queryPoint(x)returns every interval containing a point,queryRange(lo, hi)every interval overlapping a range, andhasOverlap(lo, hi)short-circuits on the first hit — allO(log n + k)instead of anO(n)scan. Built once from a fixed set, balanced by median split and augmented with each subtree's max-highso branches prune. Distinct fromIntervalSchedulingUtils(max non-overlapping subset) andweightedIntervals(DP optimizer): this is the lookup for calendar conflicts, IP-range matching, gap detection. Results come back in ascendingloworder. Roadmap #494.resolveDependencies+PackageManifest/DependencyResolution(dependency_resolver_utils.dart) — resolve a set of root requirements against a universe of available package versions (each with its own dependency constraints), picking one concrete version per package and returning a topological install order (dependencies first). Constraint matching supports*/any, caret (^1.2.0, including 0.x minor-lock semantics), the comparison operators>= <= > < == =, bare exact versions, and space-separated compound constraints (AND). ReusescompareVersionsfor ordering andtopologicalSortfor the install order + cycle detection; throwsDependencyResolutionExceptionon an unknown package, an unsatisfiable constraint set, or a cycle. Greedy highest-version over a fixpoint worklist (terminating, no backtracking — documented as simple-lock-file scope, not a SAT solver). Roadmap #540.SlaCalculator+BusinessHours/OpenWindow(sla_calculator_utils.dart) — compute SLA deadlines and elapsed working time against a weekly open-hours schedule, optionally skipping holidays via aBusinessCalendar(#593).BusinessHoursholds open windows per weekday (OpenWindow(startMinute, endMinute), half-open) with auniformfactory for the common "9-to-5 on weekdays" case.addWorkingTime(start, amount)returns the instant N working hours later (rolling across close boundaries, weekends, and holidays; clamps a before-open start to open);workingTimeBetween(start, end)sums open-hour time in a span;isOpen(at)tests a single instant. Calendar-field stepping avoids DST drift; a fully-closed schedule throwsStateErrorrather than looping. Roadmap #595.QuietHours+QuietWindow(quiet_hours_utils.dart) — "do not disturb" time-of-day blackout windows for deferring notifications.QuietWindow(startMinute, endMinute)is a daily minute-of-day window that may wrap past midnight (start > end, e.g.1320→420= 22:00–07:00).isQuiet(at)tests membership;quietUntil(at)returns the instant quiet ends whenatis muted (else null), chaining back-to-back/overlapping windows into one contiguous stretch — so a held notification can be scheduled for exactly when quiet lifts. Adailyfactory covers the single-window case. Simpler thanBusinessHours(a per-weekday OPEN schedule); quiet windows apply every day. Roadmap #613.BoundedWorkQueue<T>(bounded_work_queue_utils.dart) — a fixed-capacity async producer/consumer channel with backpressure:push(item)returns a future that stays pending while the buffer is full (throttling a fast producer to the consumer's pace) andpull()waits while empty. Hand-off is FIFO and direct (a waiting consumer receives a pushed item without it touching the buffer). Non-blockingtryPush(false when full) /tryPull(null when empty) variants, pluslength/isFull/pendingProducers/pendingConsumers.close()blocks new pushes, fails blocked producers/consumers, and lets consumers drain already-buffered items before erroring. ComplementsTaskScheduler(which is for fire-and-forget concurrency, not flow control). Roadmap #654.SlidingWindowRateLimiter(sliding_window_rate_limiter_utils.dart) — enforces "no more thanlimitevents in any trailingwindow" exactly, by keeping recent event timestamps and counting those still inside the window. The precise counterpart to the token bucket (#670): the bucket smooths to an average rate with O(1) memory and allows bursts to its capacity, this gives an exact rolling-window count with O(limit) memory and no over-limit burst.tryAcquire()records-and-allows or denies;currentCount()andtimeUntilAvailable()(when the oldest event ages out) round it out. Time is read through an injectablenowclosure for deterministic tests; the window's lower bound is exclusive (an event exactlywindowold has expired). Roadmap #685.ReadWriteLock(read_write_lock_utils.dart) — an async reader/writer lock: manyread(action)scopes run concurrently OR onewrite(action)runs exclusively, never both — the right primitive for a read-heavy cache whereAsyncMutexUtilswould needlessly serialize reads. Writer-preference by default (a waiting writer blocks newly-arriving readers, so a stream of reads can't starve a write);writerPreferred: falseswitches to reader-preference for max read throughput. Both scopes release on throw and exposeactiveReaders/isWriteLocked/waitingReaders/waitingWriters. Roadmap #653.MultiIndexCollection<T>(multi_index_collection_utils.dart) — an in-memory table maintaining several secondary hash indexes at once, so the same items can be looked up by different keys in O(1) instead of scanning (a user list queried by id, by email, AND by city). Construct with named key extractors;add/addAll/removekeep every index in sync (empty buckets pruned).getBy(index, key)returns the matching list (non-unique by default),getOneBythe first match (unique-index convenience),containsKeytests presence;all/length/indexNamesexpose the contents. Distinct frombuildInvertedIndex(text search) and the row/column transposers. Roadmap #505.evaluateExpression/evaluateBool(expression_evaluator_utils.dart) — evaluate a string expression over a supplied variable map with NO host access (no function calls, no property access, noeval), so untrusted formula input (a rule, computed column, feature-flag condition) runs without executing arbitrary code. Supports numbers,'string'/"string"literals,true/false, named variables, arithmetic (+ - * / %, unary-), comparison (== != < <= > >=), boolean (&& || !), and parentheses, with correct precedence. Lexing reuses thetokenizepipeline (#434). Any syntax error, unknown variable, or type mismatch throwsFormatException;evaluateBoolis the convenience that requires a boolean result. Roadmap #634.filterRows/compileFilter+RowPredicate(sql_filter_utils.dart) — filter a list ofMap<String, Object?>rows with a SQLWHERE-style clause (age > 18 AND city LIKE 'New%') instead of a hand-written closure.compileFilterparses the clause once into a reusableRowPredicate;filterRowsapplies it. Grammar:AND/OR/NOT/parentheses, comparisons= <> != < <= > >=,LIKE(%any run,_one char, case-sensitive),IN (…),IS [NOT] NULL, and literals (number,'string',true,false,null); keywords are case-insensitive. Ordering across incomparable types yields no match (SQL's unknown→false). Record-oriented and SQL-flavored, distinct from the scalarevaluateExpression(#634); both reuse thetokenizelexer. Roadmap #633.expandUriTemplate(url_template_utils.dart) — expand RFC 6570 URI templates against a variable map, the way an API client builds a request URL from/users/{id}{?fields*}. Covers Levels 1–3 plus the prefix ({var:3}) and explode ({list*}) modifiers: simple{var}and multiple{a,b}, reserved{+var}/ fragment{#var}(keep reserved chars), and the. / ; ? &operators with their separators andname=formatting for the named ones. Values may be string/number/bool/List; an undefined variable contributes nothing; percent-encoding is UTF-8 and RFC 3986-correct. ComplementsUriPattern(#630, which matches paths) — this builds them. Roadmap #629.LogLineParser(log_line_parser_utils.dart) — parse log lines against a format template with{field}placeholders, extracting each field into aMap<String, String>. Literal text between placeholders (spaces, brackets, quotes) delimits the fields, so the common access-log shapes parse without a bespoke regex. PresetsLogLineParser.apacheCommon()/apacheCombined()/nginxCombined()cover the standard formats; a custom format is just a template string, and a field can carry an explicit regex ({status:\d+}) when the default lazy match would over-match.parse(line)returns the field map or null on a non-match;fieldslists the names in order. Roadmap #631.
Maintenance
Changed
- Renamed the two private
_Nodehelper classes to_IntervalNode(interval_tree_utils.dart) and_TrieNode(trie_utils.dart) to clear the publish audit's duplicate-class-name flag (file-private, no public API change). - Inline WHY-comments added to the parser/iterator internals flagged by the publish audit (no behavior change). The recursive-descent levels of the expression evaluator (
_equality/_comparison/_additive/_multiplicative/_primary/_resolveIdentifier) and SQL filter (_comparison/_value/_keywordLiteral/_applyOrder), the RRULE part dispatcher (apply), the recurrence candidate generators (_candidatesFor/_dailyCandidates), the interval-tree overlap prune (_anyOverlap), the quiet-hours latest-end scan (_latestEndCovering), the URI-template expander (_expandExpression), and the INI escape decoder (_escapeChar) now carry block-level comments explaining precedence climbing, the augmented-tree pruning rules, BY-filter gating, and the type-coercion/error-path decisions a cold reader couldn't infer from the identifiers alone. CAPABILITIES.mdrebuilt from the Dart AST so it lists EVERY public symbol + a release/date header (docs/tooling only, no API change). Replaced the doc-comment-gated regex generator withtool/gen_capabilities.dart, which parses each file withpackage:analyzerand enumerates every public declaration — documented or not. This closes two gaps in the old output: undocumented public symbols were silently dropped (a container type likeDateConstantsnever appeared, and getters such asCircuitBreakerUtils.isClosedwere missing), and field initializers were mislabeled (final Duration x = Duration(...)showed up as aDurationconstructor; function-typed fields as aFunctionconstructor). Result: 1834 symbols across 383 files, up from 1528/371, with correct kinds. The title now carries**Release X.Y.Z** · Generated yyyy-mm-dd(version read frompubspec.yaml), so each snapshot is identifiable. Addedanalyzeras a directdev_dependency(already resolved transitively at^11.0.0, no new download) so the tool script can import it.publish.pystep 4 now runs the Dart generator (dart run tool/gen_capabilities.dart).
Fixed
- Quieted IDE-only stylistic-lint false positives in async/parsing utilities (no API or behavior change). The analysis-server view of
saropa_lintssurfaces opt-in stylistic-tier rules thatflutter analyze(recommended tier) does not enforce — several of which misfire on correct code:prefer_reusing_assigned_localon a recursive-descent parser's token-consuming_and()/_equality()/_not()calls (reusing the local would skip a parse),avoid_string_concatenation_loopon numeric_asNum(...) + _asNum(...), andprefer_correct_callback_field_name/prefer_correct_handler_nameon an injectednowclock, anindexersmap, astartthunk, and theisClosedstate getter. Added targeted// ignore: saropa_lints/<rule> -- reasonsuppressions (the plain form is not honored for these rules) plus two trailing commas, and filed upstream false-positive reports for the two rules whose logic is still wrong in source (prefer_reusing_assigned_local,prefer_correct_handler_name). Affected:bounded_work_queue_utils.dart,rate_limiter_utils.dart,read_write_lock_utils.dart,sliding_window_rate_limiter_utils.dart,task_scheduler_utils.dart,multi_index_collection_utils.dart,expression_evaluator_utils.dart,sql_filter_utils.dart,log_line_parser_utils.dart,url_template_utils.dart,business_calendar_utils.dart,sla_calculator_utils.dart. CAPABILITIES.mddescriptions no longer truncate at inline periods (docs/tooling only).firstSentencein tool/gen_capabilities.dart cut at the first., so any description with an inline period lost everything after it (Target false positive rate (e.g.dropped the0.01 for 1%example;Splits … (split on .dropped! ?). It now ends the sentence only on a period that sits outside backticks and balanced parentheses, is followed by a space/end, and is not an abbreviation (e.g.,i.e.,vs.) or single-letter dot, so the example survives intact.CAPABILITIES.mdno longer leaks internalroadmap #NNNmarkers (docs/tooling only). Internal backlog references in dartdoc (Async barrier: wait for N events — roadmap #676.) were emitted verbatim to the customer-facing catalog.firstSentencenow strips them after the sentence cut, with no dangling period (164 leaked references → 0).CAPABILITIES.mdno longer promotes a member's dartdoc to the file purpose (docs/tooling only). When a file had no library-level doc,filePurposeused the first member's///(e.g. a field doc onhtml/html_entity_data.dart) as the whole file's purpose. It now suppresses a leading block whose offset matches the first declaration's doc comment, so only a genuine library/floating note is used. Regression tests: test/tool/gen_capabilities_test.dart.
1.3.0 #
Ten new utilities from the Roadmap-to-700 set: a forgiving JSON-to-model reader, a CSV writer, a path-template matcher, stream zip/combine operators, multi-key grouping, a swappable cache interface with a write-through loader, date-format presets, ICU plural/select message lite, timing wrappers, and a JSON pretty-printer. log
Added #
JsonModelReader(json_model_mapper_utils.dart) — reads typed fields (requireString/requireInt/requireBool/requireDouble/requireList/optionalString/child) from decoded JSON, accumulating aValidationErrorslist instead of throwing on the first bad field. Distinguishes missing (code: 'missing') from wrong-type (code: 'type'), widensint→double, and reports nested failures with dotted paths (address.city) on a shared collection. Roadmap #637.writeCsvLine/writeCsv(csv_writer_utils.dart) — the inverse ofparseCsvLine: encode rows to CSV with RFC 4180 auto-quoting (a field is quoted only when it contains the delimiter, a quote, CR, or LF; embedded quotes are doubled). Configurabledelimiter(TSV via\t),eol(CRLF default), andforceQuote. Round-trips withparseCsvLine. Roadmap #622.UriPattern(uri_pattern_utils.dart) — compile a path template (/users/{id}/posts/{slug}) andmatch()concrete paths to extract named segment params, or null on no match. Supports a typed{id:int}constraint (matches only integer segments) and ignores leading/trailing slashes. Segment-based, no regex. Roadmap #630.zipStreams/combineLatestStreams(stream_combine_utils.dart) —zipStreamspairs two streams by index (lock-step, drops the unpaired tail);combineLatestStreamsemits the combination of the latest values whenever either source emits, once both have produced a value. Both forward errors, complete when their sources do, and cancel sources on cancel.combineLatestsubscribes lazily on listen. Roadmap #661.groupByKeys/aggregateByKeys/MultiKey(multi_key_group_utils.dart) — group an iterable by several key selectors at once into value-equalMultiKeybuckets (a rawListcan't be a map key), then optionally reduce each bucket with an aggregator (count/sum/avg per(country, year)). Preserves first-seen order. Roadmap #477.Cache<K, V>interface +WriteThroughCache(cache_interface.dart) — a commonget/set/clearcontract now implemented byLruCache,TtlCache, andSizeLimitCache, so call sites can depend on "a cache" and swap the eviction policy freely.WriteThroughCachewraps anyCachewith an async loader: a miss loads once and stores (read-through), concurrent misses for the same key share one in-flight load (thundering-herd guard), and a failed load is not cached so the next call retries. Roadmap #523.formatDateShort/formatDateMedium/formatDateLong+DateFormatNames(date_format_preset_utils.dart) — dashboard date presets without theintldependency: short is ISO2026-06-10(locale-independent, sorts lexically), medium isJun 10, 2026, long isWednesday, June 10, 2026. Month/weekday names are injected viaDateFormatNames(English default) so the presets render in any language while the layout stays fixed. Roadmap #615.icuPlural/icuSelect(icu_message_utils.dart) — ICU MessageFormat lite:icuPlural(count, {zero, one, other})picks a plural form (English cardinal rules) and substitutes#with the count;icuSelect(value, cases, {other})picks a gendered/category form. No parser, nointl. UnlikeString.pluralize(appends ans), these choose among caller-supplied, locale-routable forms. Roadmap #414.observeAsync/observeSync(observability_utils.dart) — wrap an operation to measure its wall-clock duration and report the outcome through optionalonSuccess(elapsed, result)/onError(elapsed, error, stackTrace)hooks (logging, metrics, tracing), returning the result unchanged or rethrowing the original error. Transparent: timing only, never swallows a failure. Roadmap #680.prettyPrintJson(json_pretty_print_utils.dart) — render decoded JSON as an indented string with a configurableindentwidth (0for compact single-line) and optional recursivesortKeys(viacanonicalizeJson) for stable, diff-friendly output. Roadmap #436.tokenize+TokenRule/Token(tokenizer_pipeline_utils.dart) — a reusable lexer core: walk input taking the first orderedTokenRulethat matches as a prefix, emittingToken(type, value, start)or skipping (shouldSkiprules like whitespace/comments). Rule order resolves ambiguity deterministically; an unmatched position throwsFormatExceptionwith the offset; zero-width matches are rejected so the cursor always advances. Roadmap #434.diffSequences/diffWords/diffSentences+SeqDiffOp/SeqDiffKind(text_diff_structured_utils.dart) — LCS-based structured diff returning an orderedequal/insert/deleteedit script (not a rendered string) so a UI can color or animate changes.diffWords/diffSentencesreuse the existingtokenizeWords/tokenizeSentencessplitters; the genericdiffSequences<T>engine works on any list. Roadmap #415.empiricalCdf/cdfAt/cumulativeHistogram+CdfPoint(cdf_utils.dart) — the cumulative view of numeric samples:empiricalCdfreturns oneCdfPoint(value, p)per distinct value (p = fraction ≤ value),cdfAtevaluates the CDF at a point, andcumulativeHistogramis the running total of the existinghistogramFixed. Complements the bin-counting histogram utils. Roadmap #574.validateJsonSchema+FieldSchema/JsonType(json_schema_utils.dart) — declare a JSON object as afield → FieldSchemamap and validate it in one pass, collecting aValidationErrorslist: required presence (missing), type match (type), andallowed-set/enum membership (enum). A non-map input yields a single object-level type error. Companion toJsonModelReader. Roadmap #636.groupedStats+NumericStats(grouped_stats_utils.dart) — group an iterable by a key and compute the common numeric bundle (count, sum, min, max, mean) per group in a single pass, no custom reducer. Every returned group hascount ≥ 1(no divide-by-zero inmean). The "totals and averages per category" report in one call. Roadmap #571.
Maintenance
Changed
- Lint cleanup across
lib/(no API or behavior change). Resolved 68 of the live saropa_lints violations a full insanity-tier scan surfaced: added explicit type arguments to 46 empty collection literals (<int>[],<Map<String, Object?>>[], etc.) so they match the codebase's double-explicit convention; reused an already-assigned local indeepMergeinstead of re-readingMapEntry.value; and extended the existing documented// ignoredirectives on the diagnosticdebugPrintsites inJsonUtils,Base64Utils, andasync_more_utilsto also cover the siblingavoid_debug_print/avoid_stack_trace_in_productionrules. Reworded two prose design-notes so they no longer tripprefer_no_commented_out_code, and suppressed the Flutter-app-lifecycle rules (avoid_work_in_paused_state,require_workmanager_for_background) onHeartbeatUtilswith a reason, since they do not apply to a pure-Dart timer primitive. The remainingprefer_list_firsthits are string-index false positives (Stringhas no.first) and were left unchanged. - Publish-audit accuracy + coverage (no API or behavior change). Rebuilt the
scripts/modules/audit.pydeclaration matcher and test-discovery so the publish audit stops reporting false positives: doc-header findings 94→0, recursion findings 29→0 (the check was removed — every hit was legitimate recursion), and per-method "0 tests" false positives eliminated via a global tested-identifier scan (methods tested in combined files likeduration_format_parse_test.dartare now credited). Wrote tests for the 29 genuinely-untested public methods (new*_untested_test.dart/ feature test files undertest/). Added explanatory WHY-comments to 61 of 92 under-commented function bodies acrosslib/(algorithm intent: k-way merge, A*, Dijkstra, Floyd–Warshall, knapsack, LIS, Rabin-Karp, kmeans, trial-division primes, LCS, bin-packing, inverted index, and more); 31 remain as tracked documentation debt. - Closed the remaining 31 under-commented bodies + fixed the last audit warning (no API or behavior change). Added WHY-comments to the final 31 function bodies the audit still flagged (
prefixFrequencyTable,maxWeightIntervals,rank,addBusinessDays,_buildDurationParts,roundMinutes,lowestCommonAncestor,diff,takeEveryNth/skipEveryNth,allEqual,renameKeys,upsert,getNested,abbreviateName,decodeVarint,linearRegression,simpleMovingAverage,outlierIndicesByMAD,stratifiedSampleIndices,didYouMean,stripSubstring,wrapAtChars,allIndicesOf,textFingerprint,cosineSimilarity,tokenizeSentences,pathRelative,validateOneOfRequired,parseIpv4,jwtPayload) — documenting the invariants and failure modes the names alone can't carry (window-sort precondition, MAD-zero divide guard, varint overflow bail-out, base64url padding restore, and so on), with the big-O prose phrased to avoidprefer_no_commented_out_codefalse positives. Corrected the lone live analyzer warning by prefixing theWriteThroughCachesuppression with the plugin namespace (// ignore: saropa_lints/require_cache_expiration) so it is actually honored — expiration is delegated to the wrappedCache.dart analyzeis now clean acrosslib/.
1.2.0 - 2026-06-10 #
A handful of everyday helpers: skip nulls while mapping, a readable none() check, sum or average by a selector, middle-eliding for long strings and paths, and float comparison that shrugs off rounding error. The published download is slimmer, too.
log
Added #
-
Iterable.none(predicate)(iterable_none_extensions.dart) — the boolean complement ofany; returnstruewhen no element matches (andtruefor an empty iterable, matchingevery). Reads as intent and removes the easy-to-misplace!any(...). -
Iterable.mapNotNull(selector)/Iterable<T?>.whereNotNull()(iterable_map_not_null_extensions.dart) — map-and-drop-nulls in a single lazy pass, recovering the non-nullable result type without a separatewhereType/cast. -
Iterable.sumBy(selector)/Iterable.averageBy(selector)(iterable_sum_by_extensions.dart) — numeric sum/mean over a selector, so reductions work on any element type.sumByreturns0for empty;averageByreturnsnullfor empty (no silentNaN). -
String.truncateMiddle(maxLength, {ellipsis})(string_truncate_middle_extensions.dart) — elides the middle while keeping both ends visible (paths, hashes, IDs). Grapheme-cluster safe, so emoji are never split; degrades to a leading cut when the budget is too small. -
double.isCloseTo(other, {relativeTolerance, absoluteTolerance})(double_close_to_extensions.dart) — tolerance-based float comparison ((0.1 + 0.2).isCloseTo(0.3)istrue). Combines an absolute floor (meaningful near zero) with a relative tolerance (scales to large magnitudes);NaNis never close, same-sign infinities are. -
nthSmallest/nthLargest(quickselect_utils.dart) — k-th order statistic via quickselect (median-of-three pivot), O(n) average without a full sort; returnsnullfor an out-of-range k and never mutates the input. -
Iterable.stableSortBy/stableSort(iterable_stable_sort_extensions.dart) — stable sort that preserves the input order of equal elements, unlike Dart'sList.sort(which is not guaranteed stable) — needed for correct multi-pass sorting. -
longestCommonSubsequence/longestCommonSubsequenceLength(lcs_sequence_utils.dart) — LCS of two lists (order-preserving, gaps allowed), distinct from the existing contiguous LCS-substring; the length variant uses O(min) space. -
deepFreeze(deep_freeze_utils.dart) — recursively unmodifiable copy of a map/list/set tree; any mutation at any depth throwsUnsupportedError. A copy, so later edits to the original do not show through. -
getByJsonPath(json_path_utils.dart) — read a value from decoded JSON by a simple$.a.b[0]path; returnsnullfor any missing/out-of-range segment. Deliberately not full JSONPath (no wildcards/filters/recursive descent). -
CronSchedule.tryParse+nextRunAfter(cron_utils.dart) — parse a 5-field cron expression (*, lists, ranges, steps) and compute the next run after a given time, with Vixie-cron OR semantics for the two day fields; returnsnullfor malformed expressions and for impossible schedules (no match within four years). -
parseAcceptLanguage(accept_language_utils.dart) — parse anAccept-Languageheader intoLanguageRanges ordered by quality (stable on ties); dropsq=0, skips malformed entries. -
parseRangeHeader(range_header_utils.dart) — parse an HTTPRangeheader (bytes=unit) intoByteRanges, supporting explicit, open-ended, suffix, and multi-range forms;nullon unsupported unit or any malformed range. -
canonicalizeUrl(url_canonicalize_utils.dart) — canonical URL form for dedupe/cache keys: lower-cased scheme/host, default port dropped, query parameters (and repeated values) sorted, optional fragment removal. -
debounceStream(stream_debounce_utils.dart) — re-emits stream values only after a quiet gap (latest-wins per burst); flushes the trailing pending value on close and forwards errors immediately. -
CAPABILITIES.md— a complete per-symbol index of every public utility (1,391 symbols across 352 files), grouped by category with one-line descriptions and per-file import paths, for teams evaluating or adopting the library. Covers every documented public member — methods, getters, functions, constructors, fields, enum values, setters, typedefs, classes, and extensions (verified: zero undocumented public members repo-wide). Generated bytool/gen_capabilities.pyfrom the documented public API; linked from the README's "What's Included" section.
Fixed #
- First 1.2.0 publish attempt was rejected by pub.dev validation (exit 65), so the tag/release existed but pub.dev served nothing. Two artifacts —
.favorites.json(VS Code Favorites extension state) andcoverage/lcov.info(generated byflutter test --coverage) — were tracked in git while also matching.gitignorerules.dart pub publish --dry-runemits a warning for "checked-in files ignored by a.gitignore" and exits 65, which the publish workflow treats as a hard failure (the.pubignoretarball trim does not clear this warning — it is about git tracking state, not package contents). Both files are now untracked (kept on disk); the dry run passes and 1.2.0 reached pub.dev. Nolib/changes. - 16 implemented utilities were unreachable via the barrel import. Extensions and helpers that only had direct file imports —
cartesian,diff,firstWhereOrElse,flattenDeep,groupByTransform,mapIndexed,minBy/maxBy,consecutivePairs(and the rest ofiterable_more),allPairs,sortByThenBy,splitAt,symmetricDifference,shuffleWithSeed,topK,race/allSettled/retryTimes, andGestureUtils— are now exported frompackage:saropa_dart_utils/saropa_dart_utils.dart, honoring the README's "one import" promise. A newtest/barrel_exports_test.dartimports only the barrel and exercises each, so the reachability (and absence of any method-name ambiguity) is regression-guarded. The internalhtml_entity_data.dartdata table stays unexported by design.
Maintenance
Changed
- Publish audit (
scripts/modules/audit.py) rebuilt for accuracy — ~600 noisy findings reduced to ~220 genuine ones. The audit's findings were dominated by artifacts of its regex declaration matcher and test-file mapping, not real defects. Changes:- Declaration matcher replaced with a balanced-delimiter parser. The old regex both MISSED real generic functions (
Future<T> raceFirst<T>(... Function() ...)never matched, so it was invisible to every check) and mis-captured type names from generic bounds / constructor calls / string literals (nWayMerge<T extends Comparable<…>>(was reported asComparable;= Completer<T>()asCompleter;'TrieUtils()'inside a string asTrieUtils). The parser finds the real name (the identifier before the parameter(, after an optional balanced<…>), strips string literals, and rejects names in expression/call/list-element position by the preceding character and the post-)tail. - Test discovery made global. The per-parameter check mapped each lib file to a single
<name>_test.dart, but this repo groups several lib files under one combined test (e.g.duration_format_utils.dart→duration_format_parse_test.dart), so well-tested methods reported 0 tests. It now counts references across all oftest/. Also fixed abreakthat credited only the first member per test block, zeroing the rest. - Per-parameter check redefined to "untested public methods (with parameters)". Per-parameter-variation coverage is unmeasurable by name matching, so the old "N+1 test blocks" floor was an arbitrary proxy; the measurable, meaningful signal is whether a method is referenced by ANY test.
- Non-public declarations skipped for doc/test/param checks (private members,
ClassName._()constructors, members of private types likeclass _Node); nested local closures excluded via a containment filter;@overridemembers excluded from the doc check (they inherit docs); the doc-walk now skips multi-line signature continuations. - "Possible recursion" check removed — every hit was legitimate recursion (tries, graph traversal, deep transforms) and a regex cannot do base-case analysis; the genuine empty-catch smell is kept.
- Inline-comment density no longer counts plain
final/vardeclarations (policy: "well-named identifiers cover WHAT; comment WHY on branches/loops"). - Net on this repo: missing-doc-headers 94→1 (the 1 is a local closure under a multi-line signature), recursion 29→0, untested-public-methods now 29 (all verified genuinely untested), sparse-comments 190 (genuine inline-comment debt). The remaining findings are real, not noise.
- Declaration matcher replaced with a balanced-delimiter parser. The old regex both MISSED real generic functions (
publish.pynow regeneratesCAPABILITIES.mdautomatically during release (step 4, after the remote-sync check and before formatting), so the per-symbol index can never ship stale — the release commit stages it viagit add -A. Non-fatal if the generator errors.ROADMAP_TO_400.mdreached 400/400 and was archived toplans/history/2026.06/2026.06.10/. All originally-outstanding items are implemented;ROADMAP_TO_700.mdremains the active forward roadmap.- README now carries a quality-standard banner stating the bar every utility meets — world-class lint-clean code, detailed dartdoc on every public member, and comprehensive unit-test coverage.
- Hardened today's new utilities to that bar. Documented the
LanguageRangeandByteRangeconstructors, resolved trailing-comma lints indeep_freeze_utils.dart, and reworded two comments that tripped the commented-out-code heuristic. All 15 new files passdart analyze(includingsaropa_lintsandpublic_member_api_docs) with zero issues. - Trimmed the published pub.dev tarball via
.pubignore. Repo-internal directories that no consumer needs —test/,plans/(130 files),tool/,bugs/,reports/,scripts/, and thecoverage/artifact — are now excluded from the package.lib/,example/,assets/, and the standard README/CHANGELOG/LICENSE/pubspec files remain. Takes effect on the next release; does not alter the already-published 1.1.6. - Every release in
CHANGELOG.mdandCHANGELOG_HISTORY.mdnow carries a plain-language opening line followed immediately by a[log]link to that version's tagged changelog (https://github.com/saropa/saropa_dart_utils/blob/vX.Y.Z/CHANGELOG.md;[Unreleased]points atmain). The maintenance-note template URL was corrected from thesaropa-log-capturerepo tosaropa_dart_utils. publish.py(v2.8) now enforces the release-intro/log-link convention before publishing. A new pre-check requires the release section to open with a plain-language intro line and pins its[log]link to the proposed version's tag (rewriting the[Unreleased]template'smainreference tovX.Y.Zautomatically). A missing intro prompts retry / ignore / abort, defaulting to retry so the operator can add it in an editor and re-check without restarting the run. Newversion_changeloghelpershas_release_introandupdate_log_linkcarry the logic.
1.1.6 #
A republish fix: 1.1.5's exact content reaches pub.dev now that a missing test dependency is declared. No library changes. log
Fixed #
- Release fix:
dart pub publishfailed validation (exit 65), so 1.1.5 never reached pub.dev.test/async/debounce_utils_test.dartandtest/async/heartbeat_utils_test.dartimportpackage:fake_async/fake_async.dart, butfake_asyncwas only available transitively (viaflutter_test) and was not declared inpubspec.yaml. pub.dev rejects publishing a package whose sources import an undeclared library. Addedfake_async: ^1.3.3todev_dependencies(matching the versionflutter_testresolves). Nolib/changes — this republishes the 1.1.5 content under 1.1.6.
Older versions: Entries for 1.1.5 and earlier live in CHANGELOG_HISTORY.md.
....
-+shdmNMMMMNmdhs+-
-odMMMNyo/-..``.++:+o+/-
/dMMMMMM/ `````
dMMMMMMMMNdhhhdddmmmNmmddhs+-
/MMMMMMMMMMMMMMMMMMMMMMMMMMMMMNh/
. :sdmNNNNMMMMMNNNMMMMMMMMMMMMMMMMm+
o ..~~~::~+==+~:/+sdNMMMMMMMMMMMo
m .+NMMMMMMMMMN
m+ :MMMMMMMMMm
/N: :MMMMMMMMM/
oNs. +NMMMMMMMMo
:dNy/. ./smMMMMMMMMm:
/dMNmhyso+++oosydNNMMMMMMMMMd/
.odMMMMMMMMMMMMMMMMMMMMdo-
-+shdNNMMMMNNdhs+-
``
Made by Saropa. All rights reserved.
Learn more at https://saropa.com, or mailto://dev.tools@saropa.com