feat(text, collections): Added missing functions for collections and text modules - #7229
feat(text, collections): Added missing functions for collections and text modules#7229hey-amanthakur wants to merge 19 commits into
Conversation
Groups elements of an array by a key returned by the given function. Returns a Record mapping each key to an array of matching elements.
Returns a new array with elements randomly shuffled using the Fisher-Yates algorithm. Does not mutate the original array.
Generates an array of numbers progressing from start up to but not including end, with an optional step value. Supports ascending and descending ranges via auto-detection or explicit step option.
Returns elements present in the first array but not in the second. Uses a Set for O(n) lookup performance.
Flattens an array of arrays one level deep into a single array. Does not perform deep recursive flattening.
Counts occurrences of each key returned by the given function. Returns a Record mapping each unique key to its count.
Adds 6 new function exports to the collections package: groupBy, shuffle, range, difference, flatten, countBy
Capitalizes the first character of a string and converts the rest to lowercase.
Removes ANSI escape codes from a string using a comprehensive regex pattern that covers SGR codes, cursor movements, and other terminal control sequences.
Checks if a string contains only alphabetic ASCII characters (A-Z, a-z).
Checks if a string contains only numeric ASCII characters (0-9).
Checks if a string contains only alphanumeric ASCII characters (A-Z, a-z, 0-9).
Escapes HTML special characters: & < > " and single quote. Uses named entities for &, <, >, " and the &denoland#39; numeric entity for single quote.
Reverses HTML entity escaping, converting named entities and numeric character references back to their original characters.
Adds 7 new function exports to the text package: capitalize, stripAnsi, isAlpha, isNumeric, isAlphanumeric, escapeHtml, unescapeHtml
feat(collections): add groupBy, shuffle, range, difference, flatten, countBy
feat(text): add capitalize, stripAnsi, isAlpha, isNumeric, isAlphanumeric, escapeHtml, unescapeHtml
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #7229 +/- ##
========================================
Coverage 95.00% 95.01%
========================================
Files 617 621 +4
Lines 51674 51472 -202
Branches 9326 9291 -35
========================================
- Hits 49093 48905 -188
+ Misses 2038 2029 -9
+ Partials 543 538 -5 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Thanks for drafting this. Here's my feedback:
|
collections: - groupBy: superseded by Object.groupBy() - shuffle: redundant with @std/random/shuffle - difference: superseded by Set.difference() - flatten: superseded by Array.flat() text: - capitalize: redundant with toPascalCase/toSentenceCase/toTitleCase - stripAnsi: redundant with @std/fmt/colors/stripAnsiCode - escapeHtml/unescapeHtml: redundant with @std/html
|
Hi @babiabeo, Thank you for taking the time to review my PR. I've made the requested changes, could you please take another look when you have a chance? |
|
@tomas-zijdemans thoughts on this one? |
bartlomieju
left a comment
There was a problem hiding this comment.
Thanks for this, and for trimming the original set down in response to @babiabeo's feedback — that was the right instinct.
I've left notes on a few concrete bugs below, mostly in range(). Worth saying that range() is also the function here I'm most convinced by: it's genuinely absent from std and it is not easy to write correctly, which the float-accumulation issue below demonstrates nicely.
Separately from the bugs, two things stand between this and a merge that aren't about code quality:
- New APIs need a proposal issue and maintainer sign-off before implementation — I couldn't find one for any of these five. That's on our docs to make more visible, but it does mean the API set needs agreeing before the code matters.
@std/collections(1.3.0) and@std/text(1.0.19) are both stable, so new APIs have to land asunstable_<name>.tswith an@experimentalJSDoc tag, and must not be re-exported frommod.ts. There are exemplars to copy in both packages:collections/unstable_cycle.tsandtext/unstable_slugify.ts.
None of that is wasted work — it's a relocation, not a rewrite.
|
|
||
| const result: number[] = []; | ||
| if (stepValue > 0) { | ||
| for (let i = start; i < stop; i += stepValue) { |
There was a problem hiding this comment.
Accumulating i += stepValue compounds floating-point error, so fractional steps drift and can produce an extra element. Concretely, range(0, 1, { step: 0.1 }) returns 11 elements ending in 0.9999999999999999, where you'd expect 10 ending at 0.9.
Computing each element from the index avoids it entirely:
const count = Math.max(0, Math.ceil((stop - start) / stepValue));
for (let i = 0; i < count; i++) result.push(start + i * stepValue);That also collapses both branches into one, since the sign of stepValue is already handled by the arithmetic. Worth a test with step: 0.1 and step: 0.2 — they're the classic float cases.
| if (stepValue === 0) { | ||
| throw new RangeError("`step` must not be zero"); | ||
| } | ||
| if (!Number.isFinite(start) || !Number.isFinite(stop)) { |
There was a problem hiding this comment.
start and stop are checked for finiteness here, but stepValue never is. range(0, 5, { step: NaN }) silently returns [] (every comparison against NaN is false, so the loop body never runs), and step: Infinity returns a single element.
Silently returning [] is the unkind failure mode — the caller gets no signal at all. Since you already throw a RangeError for step === 0 just above, extending that check to cover non-finite values would be consistent:
| if (!Number.isFinite(start) || !Number.isFinite(stop)) { | |
| if (!Number.isFinite(stepValue)) { | |
| throw new RangeError("`step` must be a finite number"); | |
| } | |
| if (!Number.isFinite(start) || !Number.isFinite(stop)) { | |
| throw new RangeError("`start` and `end` must be finite numbers"); | |
| } |
| * ``` | ||
| */ | ||
| export function isAlpha(input: string): boolean { | ||
| return /^[A-Za-z]+$/.test(input); |
There was a problem hiding this comment.
These three predicates are ASCII-only, and nothing in the name or the JSDoc says so. isAlpha("café") returns false, isAlpha("日本語") returns false, and isNumeric("-1.5") and isNumeric("١٢٣") likewise — all of which a reader of the name would reasonably expect to be true.
For a standard library shipped to a runtime with first-class Unicode support, silently ASCII-only string predicates are a trap. Two ways out: document the restriction prominently and consider names that carry it, or use Unicode property escapes (/^\p{L}+$/u, /^\p{Nd}+$/u).
Worth deciding before this lands, since it's the kind of semantics that can't be changed afterwards.
Whoa! This is a big change 😓 But yeah, let's get into it. I think this needs to be split into multiple PRs. I agree with everything Bartek flagged, and I'd go one step more conservative: proposal first, code second. The path I'd suggest: Open a proposal issue before touching the code further. Of the five, range is the one I'd bet on surviving. It's genuinely missing and hard to write correctly, as the float drift shows. countBy needs a case made against composing Object.groupBy yourself. The three text predicates I'd hold back entirely: ASCII-only semantics under Unicode-sounding names, in a package whose own internals already use \p{L} (see text/_util.ts). Once something is approved, split into two PRs, one per package. The combined feat(text, collections) title would minor-bump two stable packages in one squash. Split and scoped as feat(collections/unstable): add range, each releases as a patch and each lands (or stalls) on its own merits. Unstable files, as Bartek said. unstable_range.ts, @experimental tag, not re-exported from mod.ts. Two more bugs beyond the inline comments, for whenever the code moves. countBy silently loses counts when the key function returns "proto", because the assignment hits the prototype setter and never creates an own property. Accumulating in a Map and returning Object.fromEntries(map) fixes it and deletes the hasOwnProperty branch too. And range's auto-descending default is neither documented nor tested: range(5, 0) walks downward and no test or JSDoc line says so. That one is an API decision, so it belongs in the proposal, not in review comments. |
Added groupBy, shuffle, range, difference, flatten, countBy
groupBy(array, keyFn): Groups elements by a key function into a
Record<K, T[]>. The most requested collection utility across all
ecosystems (Lodash, Kotlin, etc.).
shuffle(array): Returns a new array with elements randomly permuted
using the Fisher-Yates algorithm. Does not mutate the input.
range(startOrEnd, end?, {step?}): Generates a sequence of numbers
from start (default 0) up to but not including end. Auto-detects
direction (ascending/descending) when step is omitted.
difference(a, b): Returns elements present in array a but not in
array b. Uses a Set for O(n) lookup performance.
flatten(array): Flattens an array of arrays one level deep into a
single array. Does not perform recursive deep flattening.
countBy(array, keyFn): Counts occurrences of each key returned by
the key function, returning a Record<K, number>.
Added capitalize, stripAnsi, isAlpha, isNumeric, isAlphanumeric, escapeHtml, unescapeHtml
capitalize(input): Capitalizes the first character and lowercases
the rest of the string.
stripAnsi(input): Removes ANSI escape codes from terminal strings
using a comprehensive regex covering SGR, cursor, and control sequences.
isAlpha(input): Checks if a string contains only ASCII alphabetic
characters (A-Z, a-z).
isNumeric(input): Checks if a string contains only ASCII numeric
characters (0-9).
isAlphanumeric(input): Checks if a string contains only ASCII
alphanumeric characters (A-Z, a-z, 0-9).
escapeHtml(input): Escapes HTML special characters (&, <, >, ", ')
using named and numeric character references.
unescapeHtml(input): Reverses HTML entity escaping, converting
named entities and numeric references back to original characters.