Skip to content

Commit 6e57c9c

Browse files
committed
Don't generate the same character twice in a row
In randomCharFromSetCensorStrategy(). This produces more interesting strings and avoids generating "@$$" as a side-effect. Fixes #82
1 parent 6059905 commit 6e57c9c

File tree

2 files changed

+26
-7
lines changed

2 files changed

+26
-7
lines changed

src/censor/BuiltinStrategies.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -139,25 +139,32 @@ export function fixedCharCensorStrategy(char: string): TextCensorStrategy {
139139

140140
/**
141141
* A text censoring strategy that generates replacement strings made up of
142-
* random characters from the set of characters provided.
142+
* random characters from the set of characters provided. The strings never
143+
* contain two of the same character in a row.
143144
*
144145
* @example
145146
* ```typescript
146147
* const strategy = randomCharFromSetCensorStrategy('$#!');
147148
* const censor = new TextCensor().setStrategy(strategy);
148149
* // Before: 'fuck you!'
149-
* // After: '!##$ you!'
150+
* // After: '!#$# you!'
150151
* ```
151152
* @param charset - Set of characters from which the replacement string should
152-
* be constructed. Must not be empty.
153+
* be constructed. Must have at least two characters.
153154
* @returns A [[TextCensorStrategy]] for use with the [[TextCensor]].
154155
*/
155156
export function randomCharFromSetCensorStrategy(charset: string): TextCensorStrategy {
156157
const chars = [...charset];
157-
if (chars.length === 0) throw new Error('The character set passed must not be empty.');
158+
if (chars.length < 2) throw new Error('The character set passed must have at least 2 characters.');
158159
return (ctx: CensorContext) => {
159160
let censored = '';
160-
for (let i = 0; i < ctx.matchLength; i++) censored += chars[Math.floor(Math.random() * chars.length)];
161+
let prev = chars.length;
162+
for (let i = 0; i < ctx.matchLength; i++) {
163+
let idx = Math.floor(Math.random() * (i ? chars.length - 1 : chars.length));
164+
if (idx >= prev) idx++;
165+
prev = idx;
166+
censored += chars[idx];
167+
}
161168
return censored;
162169
};
163170
}

test/censor/BuiltinStrategies.test.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,13 @@ describe('fixedCharCensorStrategy()', () => {
130130
});
131131

132132
describe('randomCharFromSetCensorStrategy()', () => {
133-
it('should throw if the charset is empty', () => {
134-
expect(() => randomCharFromSetCensorStrategy('')).toThrow(new Error('The character set passed must not be empty.'));
133+
it('should throw if the charset has less than 2 characters', () => {
134+
expect(() => randomCharFromSetCensorStrategy('')).toThrow(
135+
new Error('The character set passed must have at least 2 characters.'),
136+
);
137+
expect(() => randomCharFromSetCensorStrategy('a')).toThrow(
138+
new Error('The character set passed must have at least 2 characters.'),
139+
);
135140
});
136141

137142
it('should work for matchLength 0', () => {
@@ -144,4 +149,11 @@ describe('randomCharFromSetCensorStrategy()', () => {
144149
const strategy = randomCharFromSetCensorStrategy(charset);
145150
expect([...strategy({ ...partialCtx, matchLength: 5 })].every((c) => charset.includes(c))).toBeTruthy();
146151
});
152+
153+
it('should not repeat the same character twice in a row', () => {
154+
const strategy = randomCharFromSetCensorStrategy('ab');
155+
for (let i = 0; i < 100; i++) {
156+
expect(['aba', 'bab']).toContain(strategy({ ...partialCtx, matchLength: 3 }));
157+
}
158+
});
147159
});

0 commit comments

Comments
 (0)