blakeembrey / blakeembrey/change-case
Case functions are not idempotent for digit→letter boundaries
- Dominant language
- TypeScript
- Stars
- 2.4k
- Forks
- 106
- PR merge metrics
- No merged PRs in 30d
Description
### Summary
Several case transformers are not idempotent — applying a function to its own output changes the result — for inputs where a digit is adjacent to a letter. `split()` treats a **digit → UPPERCASE** boundary as a word split but a **digit → lowercase** boundary as not, so re-casing already-cased text (e.g. `CONSTANT_CASE`) inserts new word breaks.
### Reproduction
```js
import { constantCase } from "change-case"; // v5, default options
constantCase("foo123bar"); // "FOO123BAR"
constantCase(constantCase("foo123bar")); // "FOO123_BAR" ← extra "_"
constantCase("a1b2c3"); // "A1B2C3"
constantCase(constantCase("a1b2c3")); // "A1_B2_C3"
constantCase("3dModel"); // "3D_MODEL"
constantCase(constantCase("3dModel")); // "3_D_MODEL"
```
### Root cause
```js
const SPLIT_LOWER_UPPER_RE = /([\p{Ll}\d])(\p{Lu})/gu;
```
`\d` is in the "before" class, so a digit followed by an UPPERCASE letter is split (`"FOO123BAR"` → `FOO123` | `BAR`), while a digit followed by a lowercase letter is not (`"foo123bar"` stays one word). Because `constantCase` uppercases its output, the second pass now matches and splits where the first didn't → `f(f(x)) !== f(x)`. The same asymmetry can surface in other transforms when re-casing already-uppercased text.
### Expected
Idempotency for the case transformers: `f(f(x)) === f(x)`.
### Possible fix
Make digit boundaries symmetric — e.g. drop `\d` from the before-class of `SPLIT_LOWER_UPPER_RE` so a digit→UPPERCASE boundary isn't split when `separateNumbers` is off (numbers already have the opt-in `splitSeparateNumbers` path). This does change output for some digit-adjacent inputs, so it's a judgement call. Happy to open a PR with tests in whichever direction you'd prefer — wanted to surface the inconsistency first.
Contributor guide
No contributing guide indexed for this repository
Research direction
Start at the SPLIT_LOWER_UPPER_RE expression and the constantCase transform described in the issue. Reproduce the listed digit-to-letter cases, decide the intended symmetric boundary behavior, and add regression coverage showing that applying each affected transformer twice produces the same result.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- tooling
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 58/100