Skip to content

Commit ae95786

Browse files
authored
feat(text/unstable): add reverse function (denoland#6410)
1 parent f53efe1 commit ae95786

File tree

6 files changed

+256
-0
lines changed

6 files changed

+256
-0
lines changed

text/_test_util.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// Copyright 2018-2025 the Deno authors. MIT license.
2+
export function generateRandomString(min: number, max: number): string {
3+
return Array.from({ length: Math.floor(Math.random() * (max - min) + min) })
4+
.map(() => String.fromCharCode(Math.floor(Math.random() * 26) + 97))
5+
.join("");
6+
}

text/_test_util_test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Copyright 2018-2025 the Deno authors. MIT license.
2+
import { assertEquals } from "../assert/equals.ts";
3+
import { generateRandomString } from "./_test_util.ts";
4+
5+
Deno.test({
6+
name: "generateRandomString() generates a string of the correct length",
7+
fn() {
8+
assertEquals(generateRandomString(0, 0), "");
9+
assertEquals(generateRandomString(10, 10).length, 10);
10+
},
11+
});

text/deno.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"./closest-string": "./closest_string.ts",
77
"./compare-similarity": "./compare_similarity.ts",
88
"./levenshtein-distance": "./levenshtein_distance.ts",
9+
"./unstable-reverse": "./unstable_reverse.ts",
910
"./unstable-slugify": "./unstable_slugify.ts",
1011
"./to-camel-case": "./to_camel_case.ts",
1112
"./unstable-to-constant-case": "./unstable_to_constant_case.ts",

text/unstable_reverse.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
// Copyright 2018-2025 the Deno authors. MIT license.
2+
// This module is browser compatible.
3+
4+
// Copyright Mathias Bynens <https://mathiasbynens.be/>
5+
//
6+
// Permission is hereby granted, free of charge, to any person obtaining
7+
// a copy of this software and associated documentation files (the
8+
// "Software"), to deal in the Software without restriction, including
9+
// without limitation the rights to use, copy, modify, merge, publish,
10+
// distribute, sublicense, and/or sell copies of the Software, and to
11+
// permit persons to whom the Software is furnished to do so, subject to
12+
// the following conditions:
13+
//
14+
// The above copyright notice and this permission notice shall be
15+
// included in all copies or substantial portions of the Software.
16+
//
17+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18+
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19+
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20+
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
21+
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
22+
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
23+
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24+
25+
const REGEX_SYMBOL_WITH_COMBINING_MARKS = /(\P{M})(\p{M}+)/gu;
26+
const REGEX_SURROGATE_PAIR = /([\uD800-\uDBFF])([\uDC00-\uDFFF])/g;
27+
28+
/** Options for {@linkcode reverse} */
29+
export type ReverseOptions = {
30+
/**
31+
* Whether to handle Unicode symbols such as 🦕 at the cost of ~60% slowdown.
32+
*
33+
* Check {@link ./unstable_reverse_bench.ts} for performance comparison.
34+
*
35+
* @default {true}
36+
*/
37+
handleUnicode: boolean;
38+
};
39+
40+
/**
41+
* Performs a Unicode-aware string reversal.
42+
*
43+
* @experimental **UNSTABLE**: New API, yet to be vetted.
44+
*
45+
* @param input - The input string to be reversed.
46+
* @param options The options for the reverse function.
47+
* @returns The reversed string.
48+
*
49+
* @example Standard usage
50+
* ```ts
51+
* import { reverse } from "@std/text/unstable-reverse";
52+
* import { assertEquals } from "@std/assert";
53+
*
54+
* assertEquals(reverse("Hello, world!"), "!dlrow ,olleH");
55+
* assertEquals(reverse("🦕Deno♥"), "♥oneD🦕");
56+
* ```
57+
*
58+
* @example Performance optimization with disabled Unicode handling
59+
* ```ts
60+
* import { reverse } from "@std/text/unstable-reverse";
61+
* import { assertEquals } from "@std/assert";
62+
*
63+
* assertEquals(reverse("Hello, world!", { handleUnicode: false }), "!dlrow ,olleH");
64+
* ```
65+
*/
66+
export function reverse(
67+
input: string,
68+
options?: Partial<ReverseOptions>,
69+
): string {
70+
if (options?.handleUnicode !== false) {
71+
// Step 1: deal with combining marks and astral symbols (surrogate pairs)
72+
input = input
73+
// Swap symbols with their combining marks so the combining marks go first
74+
.replace(REGEX_SYMBOL_WITH_COMBINING_MARKS, (_, $1, $2) => {
75+
// Reverse the combining marks so they will end up in the same order
76+
// later on (after another round of reversing)
77+
return reverse($2) + $1;
78+
})
79+
// Swap high and low surrogates so the low surrogates go first
80+
.replace(REGEX_SURROGATE_PAIR, "$2$1");
81+
}
82+
83+
// Step 2: reverse the code units in the string
84+
let result = "";
85+
for (let index = input.length; index--;) {
86+
result += input.charAt(index);
87+
}
88+
return result;
89+
}

text/unstable_reverse_bench.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// Copyright 2018-2025 the Deno authors. MIT license.
2+
import { generateRandomString } from "./_test_util.ts";
3+
import { reverse } from "./unstable_reverse.ts";
4+
5+
function splitReverseJoin(str: string) {
6+
return str.split("").reverse().join("");
7+
}
8+
9+
function forOf(str: string) {
10+
let reversed = "";
11+
for (const character of str) {
12+
reversed = character + reversed;
13+
}
14+
return reversed;
15+
}
16+
17+
function reduce(str: string) {
18+
return str.split("").reduce(
19+
(reversed, character) => character + reversed,
20+
"",
21+
);
22+
}
23+
24+
function spreadReverseJoin(str: string) {
25+
return [...str].reverse().join("");
26+
}
27+
28+
function forLoop(str: string) {
29+
let x = "";
30+
31+
for (let i = str.length - 1; i >= 0; --i) {
32+
x += str[i];
33+
}
34+
35+
return x;
36+
}
37+
38+
const strings = Array.from({ length: 10000 }).map(() =>
39+
generateRandomString(0, 100)
40+
);
41+
42+
Deno.bench({
43+
group: "reverseString",
44+
name: "splitReverseJoin",
45+
fn: () => {
46+
for (let i = 0; i < strings.length; i++) {
47+
splitReverseJoin(strings[i]!);
48+
}
49+
},
50+
});
51+
Deno.bench({
52+
group: "reverseString",
53+
name: "forOf",
54+
fn: () => {
55+
for (let i = 0; i < strings.length; i++) {
56+
forOf(strings[i]!);
57+
}
58+
},
59+
});
60+
Deno.bench({
61+
group: "reverseString",
62+
name: "reduce",
63+
fn: () => {
64+
for (let i = 0; i < strings.length; i++) {
65+
reduce(strings[i]!);
66+
}
67+
},
68+
});
69+
Deno.bench({
70+
group: "reverseString",
71+
name: "spreadReverseJoin",
72+
fn: () => {
73+
for (let i = 0; i < strings.length; i++) {
74+
spreadReverseJoin(strings[i]!);
75+
}
76+
},
77+
});
78+
Deno.bench({
79+
group: "reverseString",
80+
name: "forLoop",
81+
fn: () => {
82+
for (let i = 0; i < strings.length; i++) {
83+
forLoop(strings[i]!);
84+
}
85+
},
86+
});
87+
Deno.bench({
88+
group: "reverseString",
89+
name: "esrever",
90+
fn: () => {
91+
for (let i = 0; i < strings.length; i++) {
92+
reverse(strings[i]!);
93+
}
94+
},
95+
});
96+
Deno.bench({
97+
group: "reverseString",
98+
name: "esrever (no unicode)",
99+
fn: () => {
100+
for (let i = 0; i < strings.length; i++) {
101+
reverse(strings[i]!, { handleUnicode: false });
102+
}
103+
},
104+
});

text/unstable_reverse_test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Copyright 2018-2025 the Deno authors. MIT license.
2+
import { assertEquals } from "@std/assert/equals";
3+
import { reverse } from "./unstable_reverse.ts";
4+
5+
function testBothAsciiAndUnicode(expected: string, input: string) {
6+
testAscii(expected, input);
7+
testUnicode(expected, input);
8+
}
9+
function testUnicode(expected: string, input: string) {
10+
assertEquals(expected, reverse(input));
11+
// check idempotency
12+
assertEquals(input, reverse(reverse(input)));
13+
// check empty object handling
14+
assertEquals(expected, reverse(input, {}));
15+
}
16+
function testAscii(expected: string, input: string) {
17+
assertEquals(expected, reverse(input, { handleUnicode: false }));
18+
// check idempotency
19+
assertEquals(
20+
input,
21+
reverse(reverse(input, { handleUnicode: false }), { handleUnicode: false }),
22+
);
23+
// check empty object handling
24+
assertEquals(expected, reverse(input, {}));
25+
}
26+
27+
Deno.test("reverse() handles empty string", () => {
28+
testBothAsciiAndUnicode("", "");
29+
});
30+
31+
Deno.test("reverse() reverses a string", () => {
32+
testBothAsciiAndUnicode("olleh", "hello");
33+
testBothAsciiAndUnicode("dlrow olleh", "hello world");
34+
});
35+
36+
// CREDIT: https://github.com/mathiasbynens/esrever/blob/14b34013dad49106ca08c0e65919f1fc8fea5331/README.md
37+
Deno.test("reverse() handles unicode strings", () => {
38+
testUnicode("Lorem ipsum 𝌆 dolor sit ameͨ͆t.", ".teͨ͆ma tis rolod 𝌆 muspi meroL");
39+
testUnicode("mañana mañana", "anañam anañam");
40+
41+
testUnicode("H̹̙̦̮͉̩̗̗ͧ̇̏̊̾Eͨ͆͒̆ͮ̃͏̷̮̣̫̤̣ ̵̞̹̻̀̉̓ͬ͑͡ͅCͯ̂͐͏̨̛͔̦̟͈̻O̜͎͍͙͚̬̝̣̽ͮ͐͗̀ͤ̍̀͢M̴̡̲̭͍͇̼̟̯̦̉̒͠Ḛ̛̙̞̪̗ͥͤͩ̾͑̔͐ͅṮ̴̷̷̗̼͍̿̿̓̽͐H̙̙̔̄͜", "H̙̙̔̄͜Ṯ̴̷̷̗̼͍̿̿̓̽͐Ḛ̛̙̞̪̗ͥͤͩ̾͑̔͐ͅM̴̡̲̭͍͇̼̟̯̦̉̒͠O̜͎͍͙͚̬̝̣̽ͮ͐͗̀ͤ̍̀͢Cͯ̂͐͏̨̛͔̦̟͈̻ ̵̞̹̻̀̉̓ͬ͑͡ͅEͨ͆͒̆ͮ̃͏̷̮̣̫̤̣H̹̙̦̮͉̩̗̗ͧ̇̏̊̾");
42+
43+
testUnicode("🦕Deno♥", "♥oneD🦕");
44+
testUnicode("안녕하세요", "요세하녕안");
45+
});

0 commit comments

Comments
 (0)