stringo 2.0.0
stringo: ^2.0.0 copied to clipboard
Zero-dependency Dart string toolkit - case conversion, word splitting, slugify, truncate, mask, and whitespace normalization.
CHANGELOG #
2.0.0 #
A performance rewrite. Every operation is now built on a single tokenizer that
reports word boundaries as index pairs and allocates nothing, and conversions
stream those boundaries straight into one StringBuffer. Case mapping runs
inline on code units for ASCII input and falls back to Dart's native Unicode
casing otherwise, so accented text is never ASCII-folded.
The package still has zero dependencies and still runs everywhere Dart does.
Native code through dart:ffi was measured and rejected: marshalling UTF-16
across the boundary costs more than the work itself, making it slower than
staying in Dart.
Case-conversion output is unchanged from 1.0.0 apart from the corrections listed under Fixed. That equivalence is verified by running the 1.0.0 implementations against the new ones over generated input.
Performance #
Measured on one machine with dart run benchmark/stringo_benchmark.dart:
| Operation | 1.0.0 | 2.0.0 | |
|---|---|---|---|
isBlank, 100,000 chars |
3,166,000 ns | 3 ns | ~1,000,000x |
isBlank, 10 chars |
516 ns | 7 ns | 79x |
toWords |
1,880 ns | 148 ns | 12.7x |
toCamelCase |
3,769 ns | 288 ns | 13.1x |
toSnakeCase |
2,111 ns | 307 ns | 6.9x |
slugify, 54 chars |
11,300 ns | 362 ns | 31x |
200,000 identifiers to snake_case |
341 ms | 56 ms | 6.1x |
removeEmptyLines, 400 indented lines |
14.6 ms | 0.12 ms | 121x |
removeEmptyLines, 8 KB unbroken run |
1,137 ms | 0.05 ms | ~22,700x |
isBlank is the outlier because 1.0.0 answered it by allocating two
whole-string copies and compiling a regex, which made it linear in the length
of the input. It is now a scan that returns at the first non-whitespace
character. If your code guarded isBlank with a length check or avoided it on
large strings, you can drop that workaround.
slugify gained its final 3x late: it emitted a one-character separator with
StringBuffer.write(String) while writing content with writeCharCode, and a
buffer that receives both runs about 2.6x slower than one kept in a single
mode. Case conversion had already been fixed for this; the transform layer had
not.
removeEmptyLines was the other pathological case. Its pattern
(?:[\t ]*(?:\r?\n|\r))+ backtracked catastrophically: inside a run of
spaces not followed by a line break, the engine consumed the whole run then
gave characters back one at a time. Cost was quadratic in the length of such a
run, so a document indented 40 spaces cost roughly ten times an unindented one,
and a 20 KB run of spaces took about seven seconds. It is now a linear scanner.
There is no longer any RegExp in the transform layer at all.
Added #
Stringo, a namespace exposing every operation as a plain static function:Stringo.snakeCase('userProfileField'). Useful for passing an operation as a value, and as an escape hatch when an extension member name collides with one your project already defines. Every extension member is a one-line delegation to it.- Precompiled patterns. Alongside the existing
Stringsources there are nowpatternAlphanumeric,patternStartsWithNumber,patternContainsDigits,patternNumeric,patternAlphabet, andpatternHasCapitalLetter. Prefer them: Dart does not cache compiled patterns, soRegExp(regexNumeric)recompiles on every call.
Removed #
isEmptyOrNullandisNotEmptyOrNull, which were exact synonyms ofisBlankandisNotBlank. Rename; behavior is identical.
Changed #
maskandinserton aString?receiver now returnString?and yieldnullfor a null receiver, instead of''and the inserted value. Null handling is now uniform across every transforming member on the nullable extensions. Append?? ''to restore the old result exactly.orEmptyandtoCharArray()keep their existing null behavior.maskvalidates its bounds before checking the receiver, so a negativevisibleStartorvisibleEndthrowsArgumentErroreven when the receiver is null.slugifytreats-,_, and whitespace identically. Any run of them, in any mix, collapses to a single separator. Previously a literal hyphen passed through untouched, so'a-b'.slugify(separator: '_')returned'a-b'; it now returns'a_b'. The default hyphen separator is unaffected.truncatecounts its suffix against the requested length instead of appending it on top.'Hello World'.truncate(5)was'Hello...'(8 characters) and is now'He...'(5). Ask forlength + suffix.lengthto reproduce the old output. A suffix longer thanlengthyields exactly the suffix.toCharArray()splits by Unicode code point rather than UTF-16 code unit, so a surrogate pair such as an emoji is no longer torn in half. It is still code points rather than grapheme clusters; usepackage:charactersfor those.
Fixed #
toWordsno longer emits empty elements.''.toWordswas['']and is now[];' a '.toWordswas['', 'a', '']and is now['a'].- A leading separator no longer capitalizes camelCase output.
'_leading'.toCamelCasereturned'Leading'because the phantom empty first word shifted the real one; it now returns'leading'.toPascalCasewas never affected. - The first character of supplementary-plane text is now cased.
capitalizeFirstLetter,lowercaseFirstLetter, andcapitalizeFirstLowerRestoperated ons[0], which is a lone surrogate for any character outside the Basic Multilingual Plane, and casing a lone surrogate does nothing. The first letter of Deseret, Osage, or Adlam text was silently left alone, which also affectedtoPascalCase,toCamelCase,toTitleCase,toTitle, and the four Pascal/camel separator variants. Characters without a case mapping, such as emoji, are unchanged as before.
1.0.0 #
Initial release.
stringo is the string toolkit extracted from
dart_helper_utils into a
standalone, zero-dependency package. dart_helper_utils 6.1.0 and later
re-export it, so existing code keeps working without an import change.
Included #
- Case conversion:
toWordsplus 15 case styles (toCamelCase,toPascalCase,toSnakeCase,toKebabCase,toDotCase,toTitleCase,toScreamingSnakeCase,toScreamingKebabCase,toPascalSnakeCase,toPascalKebabCase,toTrainCase,toCamelSnakeCase,toCamelKebabCase,toFlatCase,toScreamingCase),toTitle,capitalizeFirstLetter,lowercaseFirstLetter,capitalizeFirstLowerRest,shouldIgnoreCapitalization,tryToLowerCase,tryToUpperCase. - Transforms:
slugify,truncate,mask,normalizeWhitespace,clean,toOneLine,removeWhiteSpaces,removeEmptyLines,words,lines,nullIfEmpty,nullIfBlank,orEmpty,insert,toCharArray,equalsIgnoreCase,removeSurrounding,replaceBefore,replaceAfter. - Checks:
isBlank/isEmptyOrNull(and negations),isNumeric,isAlphabet,isAlphanumeric,startsWithNumber,containsDigits,hasCapitalLetter,hasMatch, and the six backing regex constants.
Changes from the dart_helper_utils originals #
toTitleCaseandtoTitlenow always capitalize the first word. Previously a leading stop word was lowercased, so'the lord of the rings'.toTitleCaseproduced'the Lord of the Rings'. It now produces'The Lord of the Rings', matching conventional title casing and the behavior the original documentation already described.titleCaseExceptionsis now public and backed by aSetfor constant-time lookup instead of a privateList.maskrejects negativevisibleStart/visibleEndwith anArgumentErrorinstead of failing later with aRangeError.insertis implemented withsubstringrather than aList<String>round-trip. Behavior is unchanged; out-of-range indices still throw aRangeError.- Extension type names dropped the
DHUprefix and are grouped by concern:StringCaseExtensions,NullableStringCaseExtensions,StringTransformExtensions,NullableStringTransformExtensions, andStringChecksExtensions.
Deliberately not included #
Domain validation (isValidEmail, isValidPhoneNumber, isValidUrl,
isValidUsername, isValidCurrency, isUuid, isValidIp4), MIME and file
type checks, parseDuration, and base64 helpers all remain in
dart_helper_utils. Those judge real-world formats rather than transform
text, and their correct behavior varies by project.