import { Option } from "effect" import { pipe } from "effect/Function" import * as S from "effect/String" import { assert, describe, it } from "vitest" import { assertFalse, assertNone, assertSome, assertTrue, deepStrictEqual, strictEqual } from "./utils/assert.ts" describe("String", () => { describe("String (constructor)", () => { it("converts values to strings", () => { strictEqual(S.String(123), "123") strictEqual(S.String(true), "true") strictEqual(S.String(null), "null") }) }) describe("isString", () => { it("returns true for strings", () => { assertTrue(S.isString("hello")) assertTrue(S.isString("")) }) it("returns false for non-strings", () => { assertFalse(S.isString(1)) assertFalse(S.isString(null)) assertFalse(S.isString(undefined)) assertFalse(S.isString(true)) assertFalse(S.isString({})) }) }) describe("Order", () => { it("compares strings lexicographically", () => { strictEqual(S.Order("apple", "banana"), -1) strictEqual(S.Order("banana", "apple"), 1) strictEqual(S.Order("apple", "apple"), 0) }) it("compares empty strings", () => { strictEqual(S.Order("", ""), 0) strictEqual(S.Order("", "a"), -1) strictEqual(S.Order("a", ""), 1) }) }) describe("Equivalence", () => { it("returns true for equal strings", () => { assertTrue(S.Equivalence("a", "a")) }) it("returns false for different strings", () => { assertFalse(S.Equivalence("a", "b")) }) }) describe("empty", () => { it("is the empty string", () => { strictEqual(S.empty, "") }) }) describe("concat", () => { it("data-first", () => { strictEqual(S.concat("hello", " world"), "hello world") }) it("data-last (pipeable)", () => { strictEqual(pipe("hello", S.concat(" world")), "hello world") }) it("concatenates empty strings", () => { strictEqual(S.concat("", ""), "") strictEqual(S.concat("a", ""), "a") strictEqual(S.concat("", "b"), "b") }) }) describe("toUpperCase", () => { it("converts to uppercase", () => { strictEqual(S.toUpperCase("hello"), "HELLO") }) it("handles empty string", () => { strictEqual(S.toUpperCase(""), "") }) it("pipeable", () => { strictEqual(pipe("abc", S.toUpperCase), "ABC") }) }) describe("toLowerCase", () => { it("converts to lowercase", () => { strictEqual(S.toLowerCase("HELLO"), "hello") }) it("handles empty string", () => { strictEqual(S.toLowerCase(""), "") }) }) describe("capitalize", () => { it("capitalizes first character", () => { strictEqual(S.capitalize("hello"), "Hello") }) it("handles single character", () => { strictEqual(S.capitalize("a"), "A") }) it("handles empty string", () => { strictEqual(S.capitalize(""), "") }) it("does not change already capitalized", () => { strictEqual(S.capitalize("Hello"), "Hello") }) }) describe("uncapitalize", () => { it("uncapitalizes first character", () => { strictEqual(S.uncapitalize("Hello"), "hello") }) it("handles single character", () => { strictEqual(S.uncapitalize("A"), "a") }) it("handles empty string", () => { strictEqual(S.uncapitalize(""), "") }) it("preserves rest of string", () => { strictEqual(S.uncapitalize("ABC"), "aBC") }) }) describe("replace", () => { it("replaces first occurrence", () => { strictEqual(pipe("abab", S.replace("b", "c")), "acab") }) it("replaces with regex", () => { strictEqual(pipe("abc", S.replace(/b/, "d")), "adc") }) it("returns unchanged if no match", () => { strictEqual(pipe("abc", S.replace("z", "x")), "abc") }) }) describe("trim", () => { it("trims whitespace from both ends", () => { strictEqual(S.trim(" hello "), "hello") }) it("trims tabs and newlines", () => { strictEqual(S.trim("\t\nhello\t\n"), "hello") }) it("handles empty string", () => { strictEqual(S.trim(""), "") }) }) describe("trimStart", () => { it("trims leading whitespace", () => { strictEqual(S.trimStart(" hello "), "hello ") }) it("handles empty string", () => { strictEqual(S.trimStart(""), "") }) }) describe("trimEnd", () => { it("trims trailing whitespace", () => { strictEqual(S.trimEnd(" hello "), " hello") }) it("handles empty string", () => { strictEqual(S.trimEnd(""), "") }) }) describe("slice", () => { it("extracts a section", () => { strictEqual(pipe("abcd", S.slice(1, 3)), "bc") }) it("handles negative indices", () => { strictEqual(pipe("hello", S.slice(-3)), "llo") }) it("no arguments returns full string", () => { strictEqual(pipe("hello", S.slice()), "hello") }) }) describe("isEmpty", () => { it("returns true for empty string", () => { assertTrue(S.isEmpty("")) }) it("returns false for non-empty string", () => { assertFalse(S.isEmpty("a")) assertFalse(S.isEmpty(" ")) }) }) describe("isNonEmpty", () => { it("returns true for non-empty string", () => { assertTrue(S.isNonEmpty("a")) }) it("returns false for empty string", () => { assertFalse(S.isNonEmpty("")) }) }) describe("length", () => { it("returns string length", () => { strictEqual(S.length("abc"), 3) }) it("returns 0 for empty string", () => { strictEqual(S.length(""), 0) }) }) describe("split", () => { it("splits by string separator (data-last)", () => { deepStrictEqual(pipe("a,b,c", S.split(",")), ["a", "b", "c"]) }) it("splits by string separator (data-first)", () => { deepStrictEqual(S.split("hello,world", ","), ["hello", "world"]) }) it("splits by empty string", () => { deepStrictEqual(pipe("abc", S.split("")), ["a", "b", "c"]) }) it("splits empty string by empty string", () => { deepStrictEqual(pipe("", S.split("")), [""]) }) it("splits by regex", () => { deepStrictEqual(pipe("a1b2c", S.split(/\d/)), ["a", "b", "c"]) }) }) describe("includes", () => { it("returns true when substring found", () => { assertTrue(pipe("hello world", S.includes("world"))) }) it("returns false when substring not found", () => { assertFalse(pipe("hello world", S.includes("foo"))) }) it("starts searching at the given position", () => { assertFalse(pipe("hello", S.includes("hel", 1))) assertTrue(pipe("hello", S.includes("ell", 1))) }) }) describe("startsWith", () => { it("returns true when string starts with search", () => { assertTrue(pipe("hello world", S.startsWith("hello"))) }) it("returns false when string does not start with search", () => { assertFalse(pipe("hello world", S.startsWith("world"))) }) it("checks the prefix at the given position", () => { assertTrue(pipe("hello world", S.startsWith("world", 6))) }) }) describe("endsWith", () => { it("returns true when string ends with search", () => { assertTrue(pipe("hello world", S.endsWith("world"))) }) it("returns false when string does not end with search", () => { assertFalse(pipe("hello world", S.endsWith("hello"))) }) it("checks the suffix at the given end position", () => { assertTrue(pipe("hello world", S.endsWith("hello", 5))) }) }) describe("charCodeAt", () => { it("returns char code at index (data-first)", () => { assertSome(S.charCodeAt("abc", 1), 98) }) it("returns char code at index (data-last)", () => { assertSome(pipe("abc", S.charCodeAt(0)), 97) }) it("returns none for out of bounds", () => { assertNone(S.charCodeAt("abc", 10)) }) }) describe("substring", () => { it("extracts characters between indices", () => { strictEqual(pipe("abcd", S.substring(1, 3)), "bc") }) it("extracts from start to end", () => { strictEqual(pipe("abcd", S.substring(1)), "bcd") }) }) describe("at", () => { it("returns character at index", () => { assertSome(pipe("abc", S.at(1)), "b") }) it("returns none for out of bounds", () => { assertNone(pipe("abc", S.at(10))) }) }) describe("charAt", () => { it("returns character at index (data-first)", () => { assertSome(S.charAt("abc", 1), "b") }) it("returns character at index (data-last)", () => { assertSome(pipe("abc", S.charAt(1)), "b") }) it("returns none for out of bounds", () => { assertNone(S.charAt("abc", 10)) }) }) describe("codePointAt", () => { it("returns code point at index", () => { assertSome(pipe("abc", S.codePointAt(1)), 98) }) it("returns none for out of bounds", () => { assertNone(pipe("abc", S.codePointAt(10))) }) it("handles surrogate pairs", () => { assertSome(pipe("𝟘", S.codePointAt(0)), 120792) }) }) describe("indexOf", () => { it("returns index of first occurrence", () => { assertSome(pipe("abbbc", S.indexOf("b")), 1) }) it("returns none when not found", () => { assertNone(pipe("abbbc", S.indexOf("z"))) }) }) describe("lastIndexOf", () => { it("returns index of last occurrence", () => { assertSome(pipe("abbbc", S.lastIndexOf("b")), 3) }) it("returns none when not found", () => { assertNone(pipe("abbbc", S.lastIndexOf("z"))) }) }) describe("localeCompare", () => { it("returns ordering", () => { strictEqual(pipe("a", S.localeCompare("b")), -1) strictEqual(pipe("b", S.localeCompare("a")), 1) strictEqual(pipe("a", S.localeCompare("a")), 0) }) }) describe("match", () => { it("returns matched text and native match metadata", () => { const result = pipe("hello", S.match(/l+/)) assert(Option.isSome(result)) strictEqual(result.value[0], "ll") }) it("returns none when the pattern does not match", () => { assertNone(pipe("hello", S.match(/x/))) }) }) describe("matchAll", () => { it("returns all matches", () => { const result = Array.from(pipe("hello world", S.matchAll(/l/g))) strictEqual(result.length, 3) }) }) describe("normalize", () => { it("normalizes with different forms", () => { const str = "\u1E9B\u0323" strictEqual(pipe(str, S.normalize("NFD")), "\u017F\u0323\u0307") strictEqual(pipe(str, S.normalize("NFKC")), "\u1E69") strictEqual(pipe(str, S.normalize("NFKD")), "\u0073\u0323\u0307") }) }) describe("padEnd", () => { it("pads from end with spaces", () => { strictEqual(pipe("a", S.padEnd(5)), "a ") }) it("pads from end with fill string", () => { strictEqual(pipe("a", S.padEnd(5, "_")), "a____") }) it("does not truncate if already longer", () => { strictEqual(pipe("hello", S.padEnd(3)), "hello") }) }) describe("padStart", () => { it("pads from start with spaces", () => { strictEqual(pipe("a", S.padStart(5)), " a") }) it("pads from start with fill string", () => { strictEqual(pipe("a", S.padStart(5, "_")), "____a") }) it("does not truncate if already longer", () => { strictEqual(pipe("hello", S.padStart(3)), "hello") }) }) describe("repeat", () => { it("repeats the string", () => { strictEqual(pipe("ab", S.repeat(3)), "ababab") }) it("returns empty for 0 repeats", () => { strictEqual(pipe("ab", S.repeat(0)), "") }) }) describe("replaceAll", () => { it("replaces all occurrences with string", () => { strictEqual(pipe("ababb", S.replaceAll("b", "c")), "acacc") }) it("replaces all occurrences with regex", () => { strictEqual(pipe("ababb", S.replaceAll(/b/g, "c")), "acacc") }) }) describe("search", () => { it("returns index on match (data-first)", () => { assertSome(S.search("ababb", "b"), 1) }) it("returns index on match (data-last)", () => { assertSome(pipe("ababb", S.search("b")), 1) }) it("returns none when not found", () => { assertNone(S.search("ababb", "d")) }) it("works with regex", () => { assertSome(S.search("ababb", /bb/), 3) }) }) describe("toLocaleLowerCase", () => { it("converts to locale lowercase", () => { strictEqual(pipe("ABC", S.toLocaleLowerCase()), "abc") }) }) describe("toLocaleUpperCase", () => { it("converts to locale uppercase", () => { strictEqual(pipe("abc", S.toLocaleUpperCase()), "ABC") }) }) describe("takeLeft", () => { it("takes n characters from the start (data-first)", () => { strictEqual(S.takeLeft("Hello World", 5), "Hello") }) it("takes n characters from the start (data-last)", () => { strictEqual(pipe("Hello World", S.takeLeft(5)), "Hello") }) it("returns whole string if n > length", () => { strictEqual(S.takeLeft("abc", 10), "abc") }) it("returns empty string for n <= 0", () => { strictEqual(S.takeLeft("abc", 0), "") strictEqual(S.takeLeft("abc", -1), "") }) }) describe("takeRight", () => { it("takes n characters from the end (data-first)", () => { strictEqual(S.takeRight("Hello World", 5), "World") }) it("takes n characters from the end (data-last)", () => { strictEqual(pipe("Hello World", S.takeRight(5)), "World") }) it("returns whole string if n > length", () => { strictEqual(S.takeRight("abc", 10), "abc") }) it("returns empty string for n <= 0", () => { strictEqual(S.takeRight("abc", 0), "") strictEqual(S.takeRight("abc", -1), "") }) }) describe("linesIterator", () => { it("yields lines without separators", () => { deepStrictEqual(Array.from(S.linesIterator("hello\nworld\n")), ["hello", "world"]) }) it("handles CRLF", () => { deepStrictEqual(Array.from(S.linesIterator("a\r\nb\r\n")), ["a", "b"]) }) it("handles single line without newline", () => { deepStrictEqual(Array.from(S.linesIterator("hello")), ["hello"]) }) it("handles empty string", () => { deepStrictEqual(Array.from(S.linesIterator("")), []) }) }) describe("linesWithSeparators", () => { it("yields lines with separators", () => { deepStrictEqual(Array.from(S.linesWithSeparators("hello\nworld\n")), ["hello\n", "world\n"]) }) it("handles CRLF", () => { deepStrictEqual(Array.from(S.linesWithSeparators("a\r\nb\r\n")), ["a\r\n", "b\r\n"]) }) it("handles line without trailing newline", () => { deepStrictEqual(Array.from(S.linesWithSeparators("hello")), ["hello"]) }) }) describe("stripMargin", () => { it("strips | margin", () => { strictEqual(S.stripMargin(" |hello\n |world"), "hello\nworld") }) }) describe("stripMarginWith", () => { it("strips custom margin character (data-first)", () => { strictEqual(S.stripMarginWith(" #hello\n #world", "#"), "hello\nworld") }) it("strips custom margin character (data-last)", () => { strictEqual(pipe(" #hello\n #world", S.stripMarginWith("#")), "hello\nworld") }) it("leaves lines without margin unchanged", () => { strictEqual(S.stripMarginWith("no margin\n |yes", "|"), "no margin\nyes") }) }) describe("snakeToCamel", () => { it("converts snake_case to camelCase", () => { strictEqual(S.snakeToCamel("hello_world"), "helloWorld") strictEqual(S.snakeToCamel("foo_bar_baz"), "fooBarBaz") }) it("handles single word", () => { strictEqual(S.snakeToCamel("hello"), "hello") }) }) describe("snakeToPascal", () => { it("converts snake_case to PascalCase", () => { strictEqual(S.snakeToPascal("hello_world"), "HelloWorld") strictEqual(S.snakeToPascal("foo_bar_baz"), "FooBarBaz") }) }) describe("snakeToKebab", () => { it("converts snake_case to kebab-case", () => { strictEqual(S.snakeToKebab("hello_world"), "hello-world") strictEqual(S.snakeToKebab("foo_bar_baz"), "foo-bar-baz") }) }) describe("camelToSnake", () => { it("converts camelCase to snake_case", () => { strictEqual(S.camelToSnake("helloWorld"), "hello_world") strictEqual(S.camelToSnake("fooBarBaz"), "foo_bar_baz") }) }) describe("pascalToSnake", () => { it("converts PascalCase to snake_case", () => { strictEqual(S.pascalToSnake("HelloWorld"), "hello_world") strictEqual(S.pascalToSnake("FooBarBaz"), "foo_bar_baz") }) }) describe("kebabToSnake", () => { it("converts kebab-case to snake_case", () => { strictEqual(S.kebabToSnake("hello-world"), "hello_world") strictEqual(S.kebabToSnake("foo-bar-baz"), "foo_bar_baz") }) }) describe("noCase", () => { it("converts to space-separated lowercase by default", () => { strictEqual(S.noCase("helloWorld"), "hello world") strictEqual(S.noCase("HelloWorld"), "hello world") }) it("data-last with options", () => { strictEqual(pipe("helloWorld", S.noCase({ delimiter: "-" })), "hello-world") }) it("handles underscores and hyphens", () => { strictEqual(S.noCase("hello_world"), "hello world") strictEqual(S.noCase("hello-world"), "hello world") }) it("splits digit-letter boundaries", () => { strictEqual(S.noCase("field2value"), "field 2 value") strictEqual(S.noCase("field2Value"), "field 2 value") }) }) describe("pascalCase", () => { it("converts to PascalCase", () => { strictEqual(S.pascalCase("hello world"), "HelloWorld") strictEqual(S.pascalCase("hello_world"), "HelloWorld") strictEqual(S.pascalCase("helloWorld"), "HelloWorld") }) it("does not prefix numeric segments with underscores", () => { strictEqual(S.pascalCase("foo 2 bar"), "Foo2Bar") strictEqual(S.pascalCase("api-v2 xml"), "ApiV2Xml") }) }) describe("camelCase", () => { it("converts to camelCase", () => { strictEqual(S.camelCase("hello world"), "helloWorld") strictEqual(S.camelCase("hello_world"), "helloWorld") strictEqual(S.camelCase("HelloWorld"), "helloWorld") }) it("does not prefix numeric segments with underscores", () => { strictEqual(S.camelCase("foo 2 bar"), "foo2Bar") strictEqual(S.camelCase("api-v2 xml"), "apiV2Xml") }) }) describe("constantCase", () => { it("converts to CONSTANT_CASE", () => { strictEqual(S.constantCase("hello world"), "HELLO_WORLD") strictEqual(S.constantCase("helloWorld"), "HELLO_WORLD") strictEqual(S.constantCase("api-v2 xml"), "API_V_2_XML") }) }) describe("configCase", () => { it("converts to CONFIG_CASE", () => { strictEqual(S.configCase("hello world"), "HELLO_WORLD") strictEqual(S.configCase("helloWorld"), "HELLO_WORLD") strictEqual(S.configCase("api-v2 xml"), "API_V2_XML") strictEqual(S.configCase("field2Value"), "FIELD2_VALUE") }) }) describe("kebabCase", () => { it("converts to kebab-case", () => { strictEqual(S.kebabCase("hello world"), "hello-world") strictEqual(S.kebabCase("helloWorld"), "hello-world") }) }) describe("snakeCase", () => { it("converts to snake_case", () => { strictEqual(S.snakeCase("hello world"), "hello_world") strictEqual(S.snakeCase("helloWorld"), "hello_world") }) }) describe("ReducerConcat", () => { it("combines strings", () => { strictEqual(S.ReducerConcat.combine("a", "b"), "ab") }) it("identity with initialValue", () => { strictEqual(S.ReducerConcat.combine("a", S.ReducerConcat.initialValue), "a") strictEqual(S.ReducerConcat.combine(S.ReducerConcat.initialValue, "a"), "a") }) }) })