1/**
2 * Works with TypeScript `string` values.
3 *
4 * This module exposes common string operations in a pipe-friendly style. The
5 * helpers cover checks, comparison, concatenation, trimming, casing, slicing,
6 * padding, replacement, normalization, safe character access, search helpers
7 * that return `Option`, and joining strings through a reducer.
8 *
9 * @since 2.0.0
10 */
11
12import type { NonEmptyArray } from "./Array.ts"
13import * as Equ from "./Equivalence.ts"
14import { dual } from "./Function.ts"
15import * as readonlyArray from "./internal/array.ts"
16import * as Count from "./internal/count.ts"
17import * as number from "./Number.ts"
18import * as Option from "./Option.ts"
19import * as order from "./Order.ts"
20import type * as Ordering from "./Ordering.ts"
21import type { Refinement } from "./Predicate.ts"
22import * as predicate from "./Predicate.ts"
23import * as Reducer from "./Reducer.ts"
24
25/**
26 * Exposes the global string constructor.
27 *
28 * **When to use**
29 *
30 * Use to access native JavaScript string coercion or constructor behavior from
31 * the Effect module namespace.
32 *
33 * **Gotchas**
34 *
35 * Calling `String(value)` returns a primitive string. Calling
36 * `new String(value)` creates a boxed `String` object.
37 *
38 * @see {@link isString} for checking whether a value is a primitive string
39 *
40 * @category constructors
41 * @since 4.0.0
42 */
43export const String = globalThis.String
44
45/**
46 * Checks whether a value is a `string`.
47 *
48 * **Example** (Checking for strings)
49 *
50 * ```ts import.meta.vitest
51 * import { String } from "effect"
52 *
53 * String.isString("a") // => true
54 * String.isString(1) // => false
55 * ```
56 *
57 * @category guards
58 * @since 2.0.0
59 */
60export const isString: Refinement<unknown, string> = predicate.isString
61
62/**
63 * Provides an `Order` instance for comparing strings using lexicographic
64 * ordering.
65 *
66 * **Example** (Comparing strings lexicographically)
67 *
68 * ```ts import.meta.vitest
69 * import { String } from "effect"
70 *
71 * String.Order("apple", "banana") // => -1
72 * String.Order("banana", "apple") // => 1
73 * String.Order("apple", "apple") // => 0
74 * ```
75 *
76 * @category instances
77 * @since 2.0.0
78 */
79export const Order: order.Order<string> = order.String
80
81/**
82 * Provides an `Equivalence` instance for strings using strict equality (`===`).
83 *
84 * **Example** (Comparing strings for equality)
85 *
86 * ```ts import.meta.vitest
87 * import { String } from "effect"
88 *
89 * String.Equivalence("hello", "hello") // => true
90 * String.Equivalence("hello", "world") // => false
91 * ```
92 *
93 * @category instances
94 * @since 2.0.0
95 */
96export const Equivalence: Equ.Equivalence<string> = Equ.String
97
98/**
99 * Provides the empty string `""`.
100 *
101 * **When to use**
102 *
103 * Use when you need the canonical empty string value from the `String` module.
104 *
105 * **Example** (Referencing the empty string)
106 *
107 * ```ts import.meta.vitest
108 * import { String } from "effect"
109 *
110 * String.empty // => ""
111 * String.isEmpty(String.empty) // => true
112 * ```
113 *
114 * @category constants
115 * @since 2.0.0
116 */
117export const empty: "" = "" as const
118
119/**
120 * Concatenates two strings at the type level.
121 *
122 * **Example** (Concatenating string literal types)
123 *
124 * ```ts import.meta.vitest
125 * import type { String } from "effect"
126 *
127 * // Type-level concatenation
128 * type Result = String.Concat<"hello", "world"> // "helloworld"
129 *
130 * const witness: Result = "helloworld"
131 * ```
132 *
133 * @category models
134 * @since 2.0.0
135 */
136export type Concat<A extends string, B extends string> = `${A}${B}`
137
138/**
139 * Concatenates two strings at runtime.
140 *
141 * **Example** (Concatenating strings)
142 *
143 * ```ts import.meta.vitest
144 * import { pipe, String } from "effect"
145 *
146 * String.concat("hello", "world") // => "helloworld"
147 * pipe("hello", String.concat("world")) // => "helloworld"
148 * ```
149 *
150 * @category combining
151 * @since 2.0.0
152 */
153export const concat: {
154 <B extends string>(that: B): <A extends string>(self: A) => Concat<A, B>
155 <A extends string, B extends string>(self: A, that: B): Concat<A, B>
156} = dual(2, (self: string, that: string): string => self + that)
157
158/**
159 * Converts a string to uppercase.
160 *
161 * **Example** (Converting strings to uppercase)
162 *
163 * ```ts import.meta.vitest
164 * import { pipe, String } from "effect"
165 *
166 * pipe("a", String.toUpperCase) // => "A"
167 * String.toUpperCase("hello") // => "HELLO"
168 * ```
169 *
170 * @category transforming
171 * @since 2.0.0
172 */
173export const toUpperCase = <S extends string>(self: S): Uppercase<S> => self.toUpperCase() as Uppercase<S>
174
175/**
176 * Converts a string to lowercase.
177 *
178 * **Example** (Converting strings to lowercase)
179 *
180 * ```ts import.meta.vitest
181 * import { pipe, String } from "effect"
182 *
183 * pipe("A", String.toLowerCase) // => "a"
184 * String.toLowerCase("HELLO") // => "hello"
185 * ```
186 *
187 * @category transforming
188 * @since 2.0.0
189 */
190export const toLowerCase = <T extends string>(self: T): Lowercase<T> => self.toLowerCase() as Lowercase<T>
191
192/**
193 * Capitalizes the first character of a string.
194 *
195 * **Example** (Capitalizing a string)
196 *
197 * ```ts import.meta.vitest
198 * import { pipe, String } from "effect"
199 *
200 * pipe("abc", String.capitalize) // => "Abc"
201 * String.capitalize("hello") // => "Hello"
202 * ```
203 *
204 * @category transforming
205 * @since 2.0.0
206 */
207export const capitalize = <T extends string>(self: T): Capitalize<T> => {
208 if (self.length === 0) return self as Capitalize<T>
209
210 return (toUpperCase(self[0]) + self.slice(1)) as Capitalize<T>
211}
212
213/**
214 * Uncapitalizes the first character of a string.
215 *
216 * **Example** (Uncapitalizing a string)
217 *
218 * ```ts import.meta.vitest
219 * import { pipe, String } from "effect"
220 *
221 * pipe("ABC", String.uncapitalize) // => "aBC"
222 * String.uncapitalize("Hello") // => "hello"
223 * ```
224 *
225 * @category transforming
226 * @since 2.0.0
227 */
228export const uncapitalize = <T extends string>(self: T): Uncapitalize<T> => {
229 if (self.length === 0) return self as Uncapitalize<T>
230
231 return (toLowerCase(self[0]) + self.slice(1)) as Uncapitalize<T>
232}
233
234/**
235 * Replaces matches in a string using `String.prototype.replace`.
236 *
237 * **Details**
238 *
239 * String search values and non-global regular expressions replace the first
240 * match; global regular expressions replace every match.
241 *
242 * **Example** (Replacing a substring)
243 *
244 * ```ts import.meta.vitest
245 * import { pipe, String } from "effect"
246 *
247 * pipe("abc", String.replace("b", "d")) // => "adc"
248 * pipe("hello world", String.replace("world", "Effect")) // => "hello Effect"
249 * ```
250 *
251 * @category transforming
252 * @since 2.0.0
253 */
254export const replace = (searchValue: string | RegExp, replaceValue: string) => (self: string): string =>
255 self.replace(searchValue, replaceValue)
256
257/**
258 * Type-level representation of trimming whitespace from both ends of a string.
259 *
260 * **Example** (Trimming whitespace at the type level)
261 *
262 * ```ts import.meta.vitest
263 * import type { String } from "effect"
264 *
265 * type Result = String.Trim<" hello "> // "hello"
266 *
267 * const witness: Result = "hello"
268 * ```
269 *
270 * @category models
271 * @since 2.0.0
272 */
273export type Trim<A extends string> = TrimEnd<TrimStart<A>>
274
275/**
276 * Removes whitespace from both ends of a string.
277 *
278 * **Example** (Trimming whitespace)
279 *
280 * ```ts import.meta.vitest
281 * import { String } from "effect"
282 *
283 * String.trim(" a ") // => "a"
284 * String.trim(" hello world ") // => "hello world"
285 * ```
286 *
287 * @category transforming
288 * @since 2.0.0
289 */
290export const trim = <A extends string>(self: A): Trim<A> => self.trim() as Trim<A>
291
292/**
293 * Type-level representation of trimming whitespace from the start of a string.
294 *
295 * **Example** (Trimming leading whitespace at the type level)
296 *
297 * ```ts import.meta.vitest
298 * import type { String } from "effect"
299 *
300 * type Result = String.TrimStart<" hello"> // "hello"
301 *
302 * const witness: Result = "hello"
303 * ```
304 *
305 * @category models
306 * @since 2.0.0
307 */
308export type TrimStart<A extends string> = A extends `${" " | "\n" | "\t" | "\r"}${infer B}` ? TrimStart<B> : A
309
310/**
311 * Removes whitespace from the start of a string.
312 *
313 * **Example** (Trimming leading whitespace)
314 *
315 * ```ts import.meta.vitest
316 * import { String } from "effect"
317 *
318 * String.trimStart(" a ") // => "a "
319 * String.trimStart(" hello world") // => "hello world"
320 * ```
321 *
322 * @category transforming
323 * @since 2.0.0
324 */
325export const trimStart = <A extends string>(self: A): TrimStart<A> => self.trimStart() as TrimStart<A>
326
327/**
328 * Type-level representation of trimming whitespace from the end of a string.
329 *
330 * **Example** (Trimming trailing whitespace at the type level)
331 *
332 * ```ts import.meta.vitest
333 * import type { String } from "effect"
334 *
335 * type Result = String.TrimEnd<"hello "> // "hello"
336 *
337 * const witness: Result = "hello"
338 * ```
339 *
340 * @category models
341 * @since 2.0.0
342 */
343export type TrimEnd<A extends string> = A extends `${infer B}${" " | "\n" | "\t" | "\r"}` ? TrimEnd<B> : A
344
345/**
346 * Removes whitespace from the end of a string.
347 *
348 * **Example** (Trimming trailing whitespace)
349 *
350 * ```ts import.meta.vitest
351 * import { String } from "effect"
352 *
353 * String.trimEnd(" a ") // => " a"
354 * String.trimEnd("hello world ") // => "hello world"
355 * ```
356 *
357 * @category transforming
358 * @since 2.0.0
359 */
360export const trimEnd = <A extends string>(self: A): TrimEnd<A> => self.trimEnd() as TrimEnd<A>
361
362/**
363 * Extracts a section of a string and returns it as a new string.
364 *
365 * **Example** (Slicing strings)
366 *
367 * ```ts import.meta.vitest
368 * import { pipe, String } from "effect"
369 *
370 * pipe("abcd", String.slice(1, 3)) // => "bc"
371 * pipe("hello world", String.slice(0, 5)) // => "hello"
372 * ```
373 *
374 * @category transforming
375 * @since 2.0.0
376 */
377export const slice = (start?: number, end?: number) => (self: string): string => self.slice(start, end)
378
379/**
380 * Checks whether a `string` is empty.
381 *
382 * **Example** (Checking for empty strings)
383 *
384 * ```ts import.meta.vitest
385 * import { String } from "effect"
386 *
387 * String.isEmpty("") // => true
388 * String.isEmpty("a") // => false
389 * ```
390 *
391 * @category guards
392 * @since 2.0.0
393 */
394export const isEmpty = (self: string): self is "" => self.length === 0
395
396/**
397 * Checks whether a `string` is non-empty.
398 *
399 * **Example** (Checking for non-empty strings)
400 *
401 * ```ts import.meta.vitest
402 * import { String } from "effect"
403 *
404 * String.isNonEmpty("") // => false
405 * String.isNonEmpty("a") // => true
406 * ```
407 *
408 * @category predicates
409 * @since 2.0.0
410 */
411export const isNonEmpty = (self: string): boolean => self.length > 0
412
413/**
414 * Returns the JavaScript string length, measured in UTF-16 code units.
415 *
416 * **Example** (Getting string length)
417 *
418 * ```ts import.meta.vitest
419 * import { String } from "effect"
420 *
421 * String.length("abc") // => 3
422 * ```
423 *
424 * @category getters
425 * @since 2.0.0
426 */
427export const length = (self: string): number => self.length
428
429/**
430 * Splits a string into an array of substrings using a separator.
431 *
432 * **Example** (Splitting strings)
433 *
434 * ```ts import.meta.vitest
435 * import { pipe, String } from "effect"
436 *
437 * pipe("abc", String.split("")) // => ["a", "b", "c"]
438 * pipe("", String.split("")) // => [""]
439 * String.split("hello,world", ",") // => ["hello", "world"]
440 * ```
441 *
442 * @category transforming
443 * @since 2.0.0
444 */
445export const split: {
446 (separator: string | RegExp): (self: string) => NonEmptyArray<string>
447 (self: string, separator: string | RegExp): NonEmptyArray<string>
448} = dual(2, (self: string, separator: string | RegExp): NonEmptyArray<string> => {
449 const out = self.split(separator)
450 return readonlyArray.isArrayNonEmpty(out) ? out : [self]
451})
452
453/**
454 * Returns `true` if `searchString` appears as a substring of `self`, at one or more positions that are
455 * greater than or equal to `position`; otherwise, returns `false`.
456 *
457 * **Example** (Checking for substrings)
458 *
459 * ```ts import.meta.vitest
460 * import { pipe, String } from "effect"
461 *
462 * pipe("hello world", String.includes("world")) // => true
463 * pipe("hello world", String.includes("foo")) // => false
464 * ```
465 *
466 * @category predicates
467 * @since 2.0.0
468 */
469export const includes = (searchString: string, position?: number) => (self: string): boolean =>
470 self.includes(searchString, position)
471
472/**
473 * Returns `true` if the string starts with the specified search string.
474 *
475 * **Example** (Checking string prefixes)
476 *
477 * ```ts import.meta.vitest
478 * import { pipe, String } from "effect"
479 *
480 * pipe("hello world", String.startsWith("hello")) // => true
481 * pipe("hello world", String.startsWith("world")) // => false
482 * ```
483 *
484 * @category predicates
485 * @since 2.0.0
486 */
487export const startsWith = (searchString: string, position?: number) => (self: string): boolean =>
488 self.startsWith(searchString, position)
489
490/**
491 * Returns `true` if the string ends with the specified search string.
492 *
493 * **Example** (Checking string suffixes)
494 *
495 * ```ts import.meta.vitest
496 * import { pipe, String } from "effect"
497 *
498 * pipe("hello world", String.endsWith("world")) // => true
499 * pipe("hello world", String.endsWith("hello")) // => false
500 * ```
501 *
502 * @category predicates
503 * @since 2.0.0
504 */
505export const endsWith = (searchString: string, position?: number) => (self: string): boolean =>
506 self.endsWith(searchString, position)
507
508/**
509 * Returns the character code at the specified index safely, or `None` if the index is out of bounds.
510 *
511 * **Example** (Reading character codes)
512 *
513 * ```ts import.meta.vitest
514 * import { Option, String } from "effect"
515 *
516 * String.charCodeAt("abc", 1) // => Option.some(98)
517 * String.charCodeAt("abc", 4) // => Option.none()
518 * ```
519 *
520 * @category getters
521 * @since 2.0.0
522 */
523export const charCodeAt: {
524 (index: number): (self: string) => Option.Option<number>
525 (self: string, index: number): Option.Option<number>
526} = dual(
527 2,
528 (self: string, index: number): Option.Option<number> =>
529 Option.filter(Option.some(self.charCodeAt(index)), (charCode) => !isNaN(charCode))
530)
531
532/**
533 * Extracts characters from a string between two specified indices.
534 *
535 * **Example** (Extracting substrings)
536 *
537 * ```ts import.meta.vitest
538 * import { pipe, String } from "effect"
539 *
540 * pipe("abcd", String.substring(1)) // => "bcd"
541 * pipe("abcd", String.substring(1, 3)) // => "bc"
542 * ```
543 *
544 * @category transforming
545 * @since 2.0.0
546 */
547export const substring = (start: number, end?: number) => (self: string): string => self.substring(start, end)
548
549/**
550 * Returns the character at the specified relative index safely, or `None` if the index is out of bounds.
551 *
552 * **Example** (Accessing characters safely)
553 *
554 * ```ts import.meta.vitest
555 * import { Option, pipe, String } from "effect"
556 *
557 * pipe("abc", String.at(1)) // => Option.some("b")
558 * pipe("abc", String.at(4)) // => Option.none()
559 * ```
560 *
561 * @category getters
562 * @since 2.0.0
563 */
564export const at: {
565 (index: number): (self: string) => Option.Option<string>
566 (self: string, index: number): Option.Option<string>
567} = dual(2, (self: string, index: number): Option.Option<string> => Option.fromUndefinedOr(self.at(index)))
568
569/**
570 * Returns the character at the specified non-negative index safely, or `None` if the index is out of bounds.
571 *
572 * **Example** (Reading characters safely)
573 *
574 * ```ts import.meta.vitest
575 * import { Option, pipe, String } from "effect"
576 *
577 * pipe("abc", String.charAt(1)) // => Option.some("b")
578 * pipe("abc", String.charAt(4)) // => Option.none()
579 * ```
580 *
581 * @category getters
582 * @since 2.0.0
583 */
584export const charAt: {
585 (index: number): (self: string) => Option.Option<string>
586 (self: string, index: number): Option.Option<string>
587} = dual(
588 2,
589 (self: string, index: number): Option.Option<string> => Option.filter(Option.some(self.charAt(index)), isNonEmpty)
590)
591
592/**
593 * Returns the Unicode code point at the specified index safely, or `None` if the index is out of bounds.
594 *
595 * **Example** (Reading code points)
596 *
597 * ```ts import.meta.vitest
598 * import { Option, pipe, String } from "effect"
599 *
600 * pipe("abc", String.codePointAt(1)) // => Option.some(98)
601 * pipe("abc", String.codePointAt(10)) // => Option.none()
602 * ```
603 *
604 * @category getters
605 * @since 2.0.0
606 */
607export const codePointAt: {
608 (index: number): (self: string) => Option.Option<number>
609 (self: string, index: number): Option.Option<number>
610} = dual(2, (self: string, index: number): Option.Option<number> => Option.fromUndefinedOr(self.codePointAt(index)))
611
612/**
613 * Returns the index of the first occurrence of a substring safely, or `None` if not found.
614 *
615 * **Example** (Finding the first substring index)
616 *
617 * ```ts import.meta.vitest
618 * import { Option, pipe, String } from "effect"
619 *
620 * pipe("abbbc", String.indexOf("b")) // => Option.some(1)
621 * pipe("abbbc", String.indexOf("z")) // => Option.none()
622 * ```
623 *
624 * @category searching
625 * @since 2.0.0
626 */
627export const indexOf = (searchString: string) => (self: string): Option.Option<number> =>
628 Option.filter(Option.some(self.indexOf(searchString)), number.isGreaterThanOrEqualTo(0))
629
630/**
631 * Returns the index of the last occurrence of a substring safely, or `None` if not found.
632 *
633 * **Example** (Finding the last substring index)
634 *
635 * ```ts import.meta.vitest
636 * import { Option, pipe, String } from "effect"
637 *
638 * pipe("abbbc", String.lastIndexOf("b")) // => Option.some(3)
639 * pipe("abbbc", String.lastIndexOf("d")) // => Option.none()
640 * ```
641 *
642 * @category searching
643 * @since 2.0.0
644 */
645export const lastIndexOf = (searchString: string) => (self: string): Option.Option<number> =>
646 Option.filter(Option.some(self.lastIndexOf(searchString)), number.isGreaterThanOrEqualTo(0))
647
648/**
649 * Computes locale-aware ordering for two strings, with optional locales and
650 * collator options, and returns the result as an `Ordering` (`-1`, `0`, or
651 * `1`).
652 *
653 * **Example** (Comparing strings by locale)
654 *
655 * ```ts import.meta.vitest
656 * import { pipe, String } from "effect"
657 *
658 * pipe("a", String.localeCompare("b")) // => -1
659 * pipe("b", String.localeCompare("a")) // => 1
660 * pipe("a", String.localeCompare("a")) // => 0
661 * ```
662 *
663 * @category comparisons
664 * @since 2.0.0
665 */
666export const localeCompare =
667 (that: string, locales?: Array<string>, options?: Intl.CollatorOptions) => (self: string): Ordering.Ordering =>
668 number.sign(self.localeCompare(that, locales, options))
669
670/**
671 * Matches a string against a pattern safely and returns `Option.some` with the match
672 * array, or `Option.none` when the pattern does not match.
673 *
674 * **Example** (Matching regular expressions)
675 *
676 * ```ts import.meta.vitest
677 * import { Option, pipe, String } from "effect"
678 *
679 * pipe(
680 * "hello",
681 * String.match(/l+/),
682 * Option.map((match) => [match[0], match.index])
683 * ) // => Option.some(["ll", 2])
684 * pipe("hello", String.match(/x/)) // => Option.none()
685 * ```
686 *
687 * @category searching
688 * @since 2.0.0
689 */
690export const match = (regExp: RegExp | string) => (self: string): Option.Option<RegExpMatchArray> =>
691 Option.fromNullOr(self.match(regExp))
692
693/**
694 * Returns an iterator over all regular expression matches in the string using
695 * native `String.prototype.matchAll` semantics.
696 *
697 * **Example** (Iterating regular expression matches)
698 *
699 * ```ts import.meta.vitest
700 * import { pipe, String } from "effect"
701 *
702 * const matches = pipe("hello world", String.matchAll(/l/g))
703 *
704 * Array.from(matches, (match) => [match[0], match.index]) // => [["l", 2], ["l", 3], ["l", 9]]
705 * ```
706 *
707 * @category searching
708 * @since 2.0.0
709 */
710export const matchAll = (regExp: RegExp) => (self: string): IterableIterator<RegExpMatchArray> => self.matchAll(regExp)
711
712/**
713 * Normalizes a string according to the specified Unicode normalization form.
714 *
715 * **Example** (Normalizing Unicode strings)
716 *
717 * ```ts import.meta.vitest
718 * import { pipe, String } from "effect"
719 *
720 * const str = "\u1E9B\u0323"
721 * Array.from(pipe(str, String.normalize()), (character) => character.codePointAt(0)) // => [0x1e9b, 0x323]
722 * Array.from(pipe(str, String.normalize("NFC")), (character) => character.codePointAt(0)) // => [0x1e9b, 0x323]
723 * Array.from(
724 * pipe(str, String.normalize("NFD")),
725 * (character) => character.codePointAt(0)
726 * ) // => [0x17f, 0x323, 0x307]
727 * Array.from(pipe(str, String.normalize("NFKC")), (character) => character.codePointAt(0)) // => [0x1e69]
728 * Array.from(
729 * pipe(str, String.normalize("NFKD")),
730 * (character) => character.codePointAt(0)
731 * ) // => [0x73, 0x323, 0x307]
732 * ```
733 *
734 * @category transforming
735 * @since 2.0.0
736 */
737export const normalize = (form?: "NFC" | "NFD" | "NFKC" | "NFKD") => (self: string): string => self.normalize(form)
738
739/**
740 * Pads the string from the end with a given fill string to a specified length.
741 *
742 * **Example** (Padding strings at the end)
743 *
744 * ```ts import.meta.vitest
745 * import { pipe, String } from "effect"
746 *
747 * pipe("a", String.padEnd(5)) // => "a "
748 * pipe("a", String.padEnd(5, "_")) // => "a____"
749 * ```
750 *
751 * @category transforming
752 * @since 2.0.0
753 */
754export const padEnd = (maxLength: number, fillString?: string) => (self: string): string =>
755 self.padEnd(maxLength, fillString)
756
757/**
758 * Pads the string from the start with a given fill string to a specified length.
759 *
760 * **Example** (Padding strings at the start)
761 *
762 * ```ts import.meta.vitest
763 * import { pipe, String } from "effect"
764 *
765 * pipe("a", String.padStart(5)) // => " a"
766 * pipe("a", String.padStart(5, "_")) // => "____a"
767 * ```
768 *
769 * @category transforming
770 * @since 2.0.0
771 */
772export const padStart = (maxLength: number, fillString?: string) => (self: string): string =>
773 self.padStart(maxLength, fillString)
774
775/**
776 * Repeats the string the specified number of times.
777 *
778 * **Details**
779 *
780 * `count` is rounded down. `NaN` and non-positive values are treated as `0`.
781 *
782 * **Example** (Repeating strings)
783 *
784 * ```ts import.meta.vitest
785 * import { pipe, String } from "effect"
786 *
787 * pipe("a", String.repeat(5)) // => "aaaaa"
788 * pipe("hello", String.repeat(3)) // => "hellohellohello"
789 * ```
790 *
791 * @category transforming
792 * @since 2.0.0
793 */
794export const repeat = (count: number) => (self: string): string => self.repeat(Count.normalize(count))
795
796/**
797 * Replaces all occurrences of a substring or pattern in a string.
798 *
799 * **Example** (Replacing all matches)
800 *
801 * ```ts import.meta.vitest
802 * import { pipe, String } from "effect"
803 *
804 * pipe("ababb", String.replaceAll("b", "c")) // => "acacc"
805 * pipe("ababb", String.replaceAll(/ba/g, "cc")) // => "accbb"
806 * ```
807 *
808 * @category transforming
809 * @since 2.0.0
810 */
811export const replaceAll = (searchValue: string | RegExp, replaceValue: string) => (self: string): string =>
812 self.replaceAll(searchValue, replaceValue)
813
814/**
815 * Returns the index of the first match for a string or regular expression safely, or
816 * `Option.none` when no match is found.
817 *
818 * **Example** (Searching strings)
819 *
820 * ```ts import.meta.vitest
821 * import { Option, String } from "effect"
822 *
823 * String.search("ababb", "b") // => Option.some(1)
824 * String.search("ababb", /abb/) // => Option.some(2)
825 * String.search("ababb", "d") // => Option.none()
826 * ```
827 *
828 * @category searching
829 * @since 2.0.0
830 */
831export const search: {
832 (regExp: RegExp | string): (self: string) => Option.Option<number>
833 (self: string, regExp: RegExp | string): Option.Option<number>
834} = dual(
835 2,
836 (self: string, regExp: RegExp | string): Option.Option<number> =>
837 Option.filter(Option.some(self.search(regExp)), number.isGreaterThanOrEqualTo(0))
838)
839
840/**
841 * Converts the string to lowercase according to the specified locale.
842 *
843 * **Example** (Lowercasing strings by locale)
844 *
845 * ```ts import.meta.vitest
846 * import { pipe, String } from "effect"
847 *
848 * const str = "\u0130"
849 * pipe(str, String.toLocaleLowerCase("tr")) // => "i"
850 * ```
851 *
852 * @category transforming
853 * @since 2.0.0
854 */
855export const toLocaleLowerCase = (locale?: string | Array<string>) => (self: string): string =>
856 self.toLocaleLowerCase(locale)
857
858/**
859 * Converts the string to uppercase according to the specified locale.
860 *
861 * **Example** (Uppercasing strings by locale)
862 *
863 * ```ts import.meta.vitest
864 * import { pipe, String } from "effect"
865 *
866 * const str = "i\u0307"
867 * pipe(str, String.toLocaleUpperCase("lt-LT")) // => "I"
868 * ```
869 *
870 * @category transforming
871 * @since 2.0.0
872 */
873export const toLocaleUpperCase = (locale?: string | Array<string>) => (self: string): string =>
874 self.toLocaleUpperCase(locale)
875
876/**
877 * Keeps the specified number of characters from the start of a string.
878 *
879 * **Details**
880 *
881 * If `n` is larger than the available number of characters, the string will
882 * be returned whole.
883 *
884 * If `n` is not a positive number, including `NaN`, an empty string will be
885 * returned.
886 *
887 * If `n` is a float, it will be rounded down to the nearest integer.
888 *
889 * **Example** (Taking characters from the start)
890 *
891 * ```ts import.meta.vitest
892 * import { String } from "effect"
893 *
894 * String.takeLeft("Hello World", 5) // => "Hello"
895 * ```
896 *
897 * @category transforming
898 * @since 2.0.0
899 */
900export const takeLeft: {
901 (n: number): (self: string) => string
902 (self: string, n: number): string
903} = dual(2, (self: string, n: number): string => self.slice(0, Count.normalize(n)))
904
905/**
906 * Keeps the specified number of characters from the end of a string.
907 *
908 * **Details**
909 *
910 * If `n` is larger than the available number of characters, the string will
911 * be returned whole.
912 *
913 * If `n` is not a positive number, including `NaN`, an empty string will be
914 * returned.
915 *
916 * If `n` is a float, it will be rounded down to the nearest integer.
917 *
918 * **Example** (Taking characters from the end)
919 *
920 * ```ts import.meta.vitest
921 * import { String } from "effect"
922 *
923 * String.takeRight("Hello World", 5) // => "World"
924 * ```
925 *
926 * @category transforming
927 * @since 2.0.0
928 */
929export const takeRight: {
930 (n: number): (self: string) => string
931 (self: string, n: number): string
932} = dual(
933 2,
934 (self: string, n: number): string => self.slice(self.length - Math.min(Count.normalize(n), self.length), Infinity)
935)
936
937const CR = 0x0d
938const LF = 0x0a
939
940/**
941 * Returns an `IterableIterator` which yields each line contained within the
942 * string, trimming off the trailing newline character.
943 *
944 * **Example** (Iterating lines without separators)
945 *
946 * ```ts import.meta.vitest
947 * import { String } from "effect"
948 *
949 * Array.from(String.linesIterator("hello\nworld\n")) // => ["hello", "world"]
950 * ```
951 *
952 * @category splitting
953 * @since 2.0.0
954 */
955export const linesIterator = (self: string): LinesIterator => linesSeparated(self, true)
956
957/**
958 * Returns an `IterableIterator` which yields each line contained within the
959 * string as well as the trailing newline character.
960 *
961 * **Example** (Iterating lines with separators)
962 *
963 * ```ts import.meta.vitest
964 * import { String } from "effect"
965 *
966 * Array.from(String.linesWithSeparators("hello\nworld\n")) // => ["hello\n", "world\n"]
967 * ```
968 *
969 * @category splitting
970 * @since 2.0.0
971 */
972export const linesWithSeparators = (s: string): LinesIterator => linesSeparated(s, false)
973
974/**
975 * Strips a leading margin prefix from every line using the supplied margin
976 * character.
977 *
978 * **Example** (Stripping custom margins)
979 *
980 * ```ts import.meta.vitest
981 * import { String } from "effect"
982 *
983 * String.stripMarginWith(" |hello\n |world", "|") // => "hello\nworld"
984 * ```
985 *
986 * @category transforming
987 * @since 2.0.0
988 */
989export const stripMarginWith: {
990 (marginChar: string): (self: string) => string
991 (self: string, marginChar: string): string
992} = dual(2, (self: string, marginChar: string): string => {
993 let out = ""
994
995 for (const line of linesWithSeparators(self)) {
996 let index = 0
997
998 while (index < line.length && line.charAt(index) <= " ") {
999 index = index + 1
1000 }
1001
1002 const stripped = index < line.length && line.charAt(index) === marginChar
1003 ? line.substring(index + 1)
1004 : line
1005
1006 out = out + stripped
1007 }
1008
1009 return out
1010})
1011
1012/**
1013 * Strips a leading `|` margin prefix from every line.
1014 *
1015 * **Example** (Stripping pipe margins)
1016 *
1017 * ```ts import.meta.vitest
1018 * import { String } from "effect"
1019 *
1020 * String.stripMargin(" |hello\n |world") // => "hello\nworld"
1021 * ```
1022 *
1023 * @category transforming
1024 * @since 2.0.0
1025 */
1026export const stripMargin = (self: string): string => stripMarginWith(self, "|")
1027
1028/**
1029 * Converts a snake_case string to camelCase.
1030 *
1031 * **Example** (Converting snake_case to camelCase)
1032 *
1033 * ```ts import.meta.vitest
1034 * import { String } from "effect"
1035 *
1036 * String.snakeToCamel("hello_world") // => "helloWorld"
1037 * String.snakeToCamel("foo_bar_baz") // => "fooBarBaz"
1038 * ```
1039 *
1040 * @category transforming
1041 * @since 2.0.0
1042 */
1043export const snakeToCamel = (self: string): string => {
1044 if (self.length === 0) return self
1045 let str = self[0]
1046 for (let i = 1; i < self.length; i++) {
1047 str += self[i] === "_" ? self[++i].toUpperCase() : self[i]
1048 }
1049 return str
1050}
1051
1052/**
1053 * Converts a snake_case string to PascalCase.
1054 *
1055 * **Example** (Converting snake_case to PascalCase)
1056 *
1057 * ```ts import.meta.vitest
1058 * import { String } from "effect"
1059 *
1060 * String.snakeToPascal("hello_world") // => "HelloWorld"
1061 * String.snakeToPascal("foo_bar_baz") // => "FooBarBaz"
1062 * ```
1063 *
1064 * @category transforming
1065 * @since 2.0.0
1066 */
1067export const snakeToPascal = (self: string): string => {
1068 if (self.length === 0) return self
1069 let str = self[0].toUpperCase()
1070 for (let i = 1; i < self.length; i++) {
1071 str += self[i] === "_" ? self[++i].toUpperCase() : self[i]
1072 }
1073 return str
1074}
1075
1076/**
1077 * Converts a snake_case string to kebab-case.
1078 *
1079 * **Example** (Converting snake_case to kebab-case)
1080 *
1081 * ```ts import.meta.vitest
1082 * import { String } from "effect"
1083 *
1084 * String.snakeToKebab("hello_world") // => "hello-world"
1085 * String.snakeToKebab("foo_bar_baz") // => "foo-bar-baz"
1086 * ```
1087 *
1088 * @category transforming
1089 * @since 2.0.0
1090 */
1091export const snakeToKebab = (self: string): string => self.replace(/_/g, "-")
1092
1093/**
1094 * Converts a camelCase string to snake_case.
1095 *
1096 * **Example** (Converting camelCase to snake_case)
1097 *
1098 * ```ts import.meta.vitest
1099 * import { String } from "effect"
1100 *
1101 * String.camelToSnake("helloWorld") // => "hello_world"
1102 * String.camelToSnake("fooBarBaz") // => "foo_bar_baz"
1103 * ```
1104 *
1105 * @category transforming
1106 * @since 2.0.0
1107 */
1108export const camelToSnake = (self: string): string => self.replace(/([A-Z])/g, "_$1").toLowerCase()
1109
1110/**
1111 * Converts a PascalCase string to snake_case.
1112 *
1113 * **Example** (Converting PascalCase to snake_case)
1114 *
1115 * ```ts import.meta.vitest
1116 * import { String } from "effect"
1117 *
1118 * String.pascalToSnake("HelloWorld") // => "hello_world"
1119 * String.pascalToSnake("FooBarBaz") // => "foo_bar_baz"
1120 * ```
1121 *
1122 * @category transforming
1123 * @since 2.0.0
1124 */
1125export const pascalToSnake = (self: string): string =>
1126 (self.slice(0, 1) + self.slice(1).replace(/([A-Z])/g, "_$1")).toLowerCase()
1127
1128/**
1129 * Converts a kebab-case string to snake_case.
1130 *
1131 * **Example** (Converting kebab-case to snake_case)
1132 *
1133 * ```ts import.meta.vitest
1134 * import { String } from "effect"
1135 *
1136 * String.kebabToSnake("hello-world") // => "hello_world"
1137 * String.kebabToSnake("foo-bar-baz") // => "foo_bar_baz"
1138 * ```
1139 *
1140 * @category transforming
1141 * @since 2.0.0
1142 */
1143export const kebabToSnake = (self: string): string => self.replace(/-/g, "_")
1144
1145class LinesIterator implements IterableIterator<string> {
1146 private index: number
1147 private readonly length: number
1148 readonly s: string
1149 readonly stripped: boolean
1150
1151 constructor(
1152 s: string,
1153 stripped: boolean = false
1154 ) {
1155 this.s = s
1156 this.stripped = stripped
1157 this.index = 0
1158 this.length = s.length
1159 }
1160
1161 next(): IteratorResult<string> {
1162 if (this.done) {
1163 return { done: true, value: undefined }
1164 }
1165 const start = this.index
1166 while (!this.done && !isLineBreak(this.s[this.index]!)) {
1167 this.index = this.index + 1
1168 }
1169 let end = this.index
1170 if (!this.done) {
1171 const char = this.s[this.index]!
1172 this.index = this.index + 1
1173 if (!this.done && isLineBreak2(char, this.s[this.index]!)) {
1174 this.index = this.index + 1
1175 }
1176 if (!this.stripped) {
1177 end = this.index
1178 }
1179 }
1180 return { done: false, value: this.s.substring(start, end) }
1181 }
1182
1183 [Symbol.iterator](): IterableIterator<string> {
1184 return new LinesIterator(this.s, this.stripped)
1185 }
1186
1187 private get done(): boolean {
1188 return this.index >= this.length
1189 }
1190}
1191
1192/**
1193 * Checks whether the provided character is a line break character (i.e. either `"\r"`
1194 * or `"\n"`).
1195 */
1196const isLineBreak = (char: string): boolean => {
1197 const code = char.charCodeAt(0)
1198 return code === CR || code === LF
1199}
1200
1201/**
1202 * Checks whether the provided characters combine to form a carriage return/line-feed
1203 * (i.e. `"\r\n"`).
1204 */
1205const isLineBreak2 = (char0: string, char1: string): boolean => char0.charCodeAt(0) === CR && char1.charCodeAt(0) === LF
1206
1207const linesSeparated = (self: string, stripped: boolean): LinesIterator => new LinesIterator(self, stripped)
1208
1209/**
1210 * Normalizes a string by splitting it into word parts, transforming each part,
1211 * and joining the parts with a configurable delimiter.
1212 *
1213 * **When to use**
1214 *
1215 * Use when you need custom word-case output with a delimiter or part transform
1216 * that the fixed case helpers do not provide.
1217 *
1218 * @see {@link pascalCase} for fixed PascalCase output
1219 * @see {@link camelCase} for fixed lower-initial camelCase output
1220 * @see {@link constantCase} for fixed uppercase underscore-separated output
1221 * @see {@link kebabCase} for fixed lowercase hyphen-separated output
1222 * @see {@link snakeCase} for fixed lowercase underscore-separated output
1223 *
1224 * @category transforming
1225 * @since 4.0.0
1226 */
1227export const noCase: {
1228 (options?: {
1229 readonly splitRegExp?: RegExp | ReadonlyArray<RegExp> | undefined
1230 readonly stripRegExp?: RegExp | ReadonlyArray<RegExp> | undefined
1231 readonly delimiter?: string | undefined
1232 readonly transform?: (part: string, index: number, parts: ReadonlyArray<string>) => string
1233 }): (self: string) => string
1234 (self: string, options?: {
1235 readonly splitRegExp?: RegExp | ReadonlyArray<RegExp> | undefined
1236 readonly stripRegExp?: RegExp | ReadonlyArray<RegExp> | undefined
1237 readonly delimiter?: string | undefined
1238 readonly transform?: (part: string, index: number, parts: ReadonlyArray<string>) => string
1239 }): string
1240} = dual((args) => typeof args[0] === "string", (input: string, options?: {
1241 readonly splitRegExp?: RegExp | ReadonlyArray<RegExp> | undefined
1242 readonly stripRegExp?: RegExp | ReadonlyArray<RegExp> | undefined
1243 readonly delimiter?: string | undefined
1244 readonly transform?: (part: string, index: number, parts: ReadonlyArray<string>) => string
1245}): string => {
1246 const splitRegExp = toRegExpArray(options?.splitRegExp ?? SPLIT_REGEXP)
1247 const stripRegExp = toRegExpArray(options?.stripRegExp ?? STRIP_REGEXP)
1248 const delimiter = options?.delimiter ?? " "
1249 const transform = options?.transform ?? toLowerCase
1250 return normalizeCase(input, splitRegExp, stripRegExp, delimiter, transform)
1251})
1252
1253const toRegExpArray = (regexp: RegExp | ReadonlyArray<RegExp>): ReadonlyArray<RegExp> =>
1254 predicate.isRegExp(regexp) ? [regexp] : regexp
1255
1256const normalizeCase = (
1257 input: string,
1258 splitRegExp: ReadonlyArray<RegExp>,
1259 stripRegExp: ReadonlyArray<RegExp>,
1260 delimiter: string,
1261 transform: (part: string, index: number, parts: ReadonlyArray<string>) => string
1262): string => {
1263 let result = input
1264 for (const regexp of splitRegExp) {
1265 result = result.replace(regexp, "$1\0$2")
1266 }
1267 for (const regexp of stripRegExp) {
1268 result = result.replace(regexp, "\0")
1269 }
1270 let start = 0
1271 let end = result.length
1272 // Trim the delimiter from around the output string.
1273 while (result.charAt(start) === "\0") {
1274 start++
1275 }
1276 while (result.charAt(end - 1) === "\0") {
1277 end--
1278 }
1279
1280 // Transform each token independently.
1281 return result.slice(start, end).split("\0").map(transform).join(delimiter)
1282}
1283
1284// Support camel case ("camelCase" -> "camel Case" and "CAMELCase" -> "CAMEL Case")
1285// and digit boundaries ("camel2case" -> "camel 2 case").
1286const SPLIT_REGEXP = [/([a-z0-9])([A-Z])/g, /([A-Z])([A-Z][a-z])/g, /([A-Z])([0-9])/gi, /([0-9])([A-Z])/gi]
1287
1288// Config paths preserve digit groups such as "v2" while still supporting camel case.
1289const CONFIG_SPLIT_REGEXP = [/([a-z0-9])([A-Z])/g, /([A-Z])([A-Z][a-z])/g]
1290
1291// Remove all non-word characters.
1292const STRIP_REGEXP = /[^A-Z0-9]+/gi
1293
1294const pascalCaseTransform = (input: string): string => {
1295 const firstChar = input.charAt(0)
1296 const lowerChars = input.substring(1).toLowerCase()
1297 return `${firstChar.toUpperCase()}${lowerChars}`
1298}
1299
1300/**
1301 * Converts a string to PascalCase.
1302 *
1303 * **When to use**
1304 *
1305 * Use to normalize strings from spaces, separators, or camel/Pascal word
1306 * boundaries into PascalCase.
1307 *
1308 * @see {@link camelCase} for lower-initial camelCase output
1309 * @see {@link noCase} for configurable delimiters and part transforms
1310 * @see {@link snakeToPascal} for converting known snake_case input only
1311 *
1312 * @category transforming
1313 * @since 4.0.0
1314 */
1315export const pascalCase: (self: string) => string = noCase({
1316 delimiter: "",
1317 transform: pascalCaseTransform
1318})
1319
1320const camelCaseTransform = (input: string, index: number): string =>
1321 index === 0
1322 ? input.toLowerCase()
1323 : pascalCaseTransform(input)
1324
1325/**
1326 * Converts a string to camelCase.
1327 *
1328 * **When to use**
1329 *
1330 * Use to normalize mixed word separators or existing PascalCase/camelCase text
1331 * into lower-initial camelCase identifiers.
1332 *
1333 * @see {@link noCase} for configurable delimiters and part transforms
1334 * @see {@link pascalCase} for upper-initial PascalCase output
1335 * @see {@link snakeCase} for lowercase underscore-separated output
1336 * @see {@link kebabCase} for lowercase hyphen-separated output
1337 * @see {@link constantCase} for uppercase underscore-separated output
1338 *
1339 * @category transforming
1340 * @since 4.0.0
1341 */
1342export const camelCase: (self: string) => string = noCase({
1343 delimiter: "",
1344 transform: camelCaseTransform
1345})
1346
1347/**
1348 * Converts a string to CONSTANT_CASE (uppercase with underscores).
1349 *
1350 * **When to use**
1351 *
1352 * Use to normalize words from mixed input formats into uppercase,
1353 * underscore-separated identifiers.
1354 *
1355 * @see {@link snakeCase} for lowercase underscore-separated output
1356 * @see {@link kebabCase} for lowercase hyphen-separated output
1357 * @see {@link camelCase} for lower-initial camelCase output
1358 * @see {@link pascalCase} for upper-initial PascalCase output
1359 * @see {@link configCase} for configuration key casing that preserves numeric word groups
1360 * @see {@link noCase} for configurable delimiters and part transforms
1361 *
1362 * @category transforming
1363 * @since 4.0.0
1364 */
1365export const constantCase: (self: string) => string = noCase({
1366 delimiter: "_",
1367 transform: toUpperCase
1368})
1369
1370/**
1371 * Converts a string to CONFIG_CASE (uppercase with underscores) for
1372 * configuration keys.
1373 *
1374 * **When to use**
1375 *
1376 * Use to normalize configuration path segments into environment-variable-like
1377 * keys while preserving numeric word groups such as `v2`.
1378 *
1379 * **Details**
1380 *
1381 * Unlike {@link constantCase}, digit-letter boundaries are not split. For
1382 * example, `"api-v2 xml"` becomes `"API_V2_XML"`.
1383 *
1384 * @see {@link constantCase} for standard uppercase underscore-separated output
1385 * @category transforming
1386 * @since 4.0.0
1387 */
1388export const configCase: (self: string) => string = (self) =>
1389 normalizeCase(self, CONFIG_SPLIT_REGEXP, [STRIP_REGEXP], "_", toUpperCase)
1390
1391/**
1392 * Converts a string to kebab-case (lowercase with hyphens).
1393 *
1394 * **When to use**
1395 *
1396 * Use to normalize free-form labels, identifiers, or keys into lowercase
1397 * hyphen-separated text.
1398 *
1399 * @see {@link noCase} for configurable delimiters and part transforms
1400 * @see {@link snakeCase} for lowercase underscore-separated output
1401 * @see {@link constantCase} for uppercase underscore-separated output
1402 * @see {@link camelCase} for lower-initial camelCase output
1403 * @see {@link pascalCase} for upper-initial PascalCase output
1404 *
1405 * @category transforming
1406 * @since 4.0.0
1407 */
1408export const kebabCase: (self: string) => string = noCase({
1409 delimiter: "-"
1410})
1411
1412/**
1413 * Converts a string to snake_case (lowercase with underscores).
1414 *
1415 * **When to use**
1416 *
1417 * Use to normalize mixed-case or separator-delimited text into lowercase words
1418 * joined with underscores.
1419 *
1420 * @see {@link noCase} for configurable lower-level normalization
1421 * @see {@link kebabCase} for lowercase hyphen-separated output
1422 * @see {@link constantCase} for uppercase underscore-separated output
1423 *
1424 * @category transforming
1425 * @since 4.0.0
1426 */
1427export const snakeCase: (self: string) => string = noCase({
1428 delimiter: "_"
1429})
1430
1431/**
1432 * Reducer for concatenating `string`s.
1433 *
1434 * **When to use**
1435 *
1436 * Use to concatenate many strings through APIs that consume a `Reducer`.
1437 *
1438 * **Details**
1439 *
1440 * The reducer starts from `""`, so combining an empty collection returns `""`.
1441 *
1442 * @see {@link concat} for concatenating two strings directly
1443 *
1444 * @category combining
1445 * @since 4.0.0
1446 */
1447export const ReducerConcat: Reducer.Reducer<string> = Reducer.make((a, b) => a + b, "")