Lập trình · 23/10/2026

JavaScript Case Conversion: Why String Length Can Change

Uppercasing does not guarantee the same length or a reversible transformation. Lookup keys, highlight offsets and stored text need explicit rules beyond a call to toUpperCase().

JavaScript đổi hoa thường: vì sao độ dài chuỗi có thể thay đổi?

Uppercasing does not guarantee the same length or a reversible transformation. Lookup keys, highlight offsets and stored text need explicit rules beyond a call to toUpperCase().

1. One character can expand into several

const original = '\u00DF'; // ß
const upper = original.toUpperCase();
console.log(original.length); // 1
console.log(upper); // SS
console.log(upper.length); // 2
console.log(upper.toLowerCase() === original); // false

Here ß becomes SS, and lowercasing that result produces ss rather than the original. toUpperCase returns a new string; see MDN String.toUpperCase. Uppercasing followed by lowercasing is not a lossless round-trip test.

Measure length at the stage required by the product. If the limit applies after conversion, validate the output using the agreed unit. Original offsets may no longer identify the same position in transformed text; highlighting needs a separate position-mapping strategy.

2. Locale can change lowercase results

console.log('I'.toLowerCase()); // i
console.log('I'.toLocaleLowerCase('tr') === '\u0131'); // true
const dotted = '\u0130'; // İ
console.log(dotted.toLocaleLowerCase('en-US').length); // 2
console.log(dotted.toLocaleLowerCase('tr')); // i

Turkish rules map I to dotless ı. For İ, the en-US result in this example occupies two UTF-16 code units, while tr produces i. MDN toLocaleLowerCase documents locale-aware mappings.

Choose locale according to the field's requirements. Persistent keys should not change with each user's interface language. Frontend and backend implementations need an agreed algorithm, locale and test corpus rather than merely similar function names.

3. ASCII codes can use a narrower contract

function canonicalCode(value) {
  if (typeof value !== 'string') {
    throw new TypeError('code must be a string');
  }
  if (!/^[A-Za-z0-9-]{1,24}$/.test(value)) {
    throw new RangeError('invalid ASCII code');
  }
  return value.toLowerCase();
}
console.log(canonicalCode('HT-Ab12')); // ht-ab12
console.log(canonicalCode('HT-Ab12') === canonicalCode('ht-ab12')); // true

This helper accepts 1–24 ASCII Latin letters, digits or hyphens before lowercasing. It does not trim or coerce numbers and rejects non-ASCII text before transformation. Every allowed character maps one-to-one or remains unchanged, so this contract preserves length.

Case variants intentionally share a key. Check uniqueness on that key instead of silently overwriting records. A lone hyphen remains valid under this regex, so it is not a complete identifier format for every business. Names and multilingual text need different policies.

4. Separate original text from lookup keys

Keep the original when exact display matters and derive keys under a separate policy. Case conversion does not replace Unicode normalization, language-sensitive comparison or a full case-folding algorithm. Define what happens when multiple inputs produce the same key.

Before changing an existing policy, inspect stored data for collisions. A small code change can affect lookup relationships and unique constraints when its output changes.

5. Review before canonicalizing case

  • Is this display text or a lookup key?
  • Must the original remain available?
  • Is length measured before or after conversion, and in which unit?
  • Is locale explicit and consistent across systems?
  • How are collisions between different inputs handled?

All six bilingual blocks ran with Node.js, with additional helper length, invalid-type and non-ASCII checks. Data migrations and real highlighting interfaces were not tested.

Discussion

Comments 0

Sign in to comment

You need an account to join the discussion and reply to other readers.

Sign inRegister

No comments yet. Be the first to share your thoughts.