Humanizr/Humanizer · critical · InvalidOperationException
Linked-vigesimal parser profiles require descending scales w
Error message
Linked-vigesimal parser profiles require descending scales where each larger scale is divisible by the next smaller scale.
What it means
This InvalidOperationException is thrown at parse time by LinkedVigesimalWordsToNumberConverter.GetMaximumCountForScale when two adjacent scale rows violate the divisibility invariant: the previous (larger) scale must be strictly greater than the current scale and the previous must be evenly divisible by the current. This invariant ensures the parser can compute a maximum count for each scale tier. The check runs lazily during parsing, so it surfaces on the first ToNumber/TryConvert call that reaches a non-zero scale index, not at profile construction.
Source
Thrown at src/Humanizer/Localisation/WordsToNumber/LinkedVigesimalWordsToNumberConverter.cs:203
}
value = firstValue;
next = afterFirst;
return true;
}
ulong GetMaximumCountForScale(int scaleIndex)
{
if (scaleIndex == 0)
{
return ulong.MaxValue / (ulong)profile.TokenizedScales[scaleIndex].Value;
}
var previous = (ulong)profile.TokenizedScales[scaleIndex - 1].Value;
var current = (ulong)profile.TokenizedScales[scaleIndex].Value;
if (previous <= current || previous % current != 0)
{
throw new InvalidOperationException("Linked-vigesimal parser profiles require descending scales where each larger scale is divisible by the next smaller scale.");
}
return previous / current - 1UL;
}
int FindScaleCountEnd(string[] tokens, int start, int end, ulong parentScaleValue)
{
for (var index = start; index < end; index++)
{
for (var scaleIndex = 0; scaleIndex < profile.TokenizedScales.Length; scaleIndex++)
{
var scale = profile.TokenizedScales[scaleIndex];
if ((ulong)scale.Value < parentScaleValue &&
(TryMatchTokenPhrase(tokens, index, end, scale.NameTokens, out _) ||
TryMatchTokenPhrase(tokens, index, end, scale.NameWithRemainderTokens, out _)))
{
return index;
}View on GitHub (pinned to ffc2b77c0f)
Solutions
- Fix the locale's scale definitions so each scale is strictly descending and each larger scale is divisible by the next smaller one (e.g. 20, 400, 8000 for pure base-20).
- Regenerate the locale source from corrected YAML using the project's source generator.
- Write a unit test that constructs the profile and parses a sample phrase to catch the invariant at CI time.
- Verify scale values against the locale's numeral system documentation.
Example fix
// before (locale YAML scales) // - value: 20 // - value: 15 # not a divisor of 20 // after // - value: 20 // - value: 4 # 20 / 4 = 5, clean division
Defensive patterns
Strategy: validation
Validate before calling
// For locale authors: validate scale divisibility before registration
static bool ScalesAreDivisible(long[] scales)
{
for (var i = 1; i < scales.Length; i++)
{
if (scales[i - 1] <= scales[i] || scales[i - 1] % scales[i] != 0)
return false;
}
return true;
} Try / catch
try
{
return words.ToNumber(culture);
}
catch (InvalidOperationException ex) when (ex.Message.Contains("divisible by the next smaller scale"))
{
// Locale scale data is corrupt; the profile itself is misconfigured
throw new InvalidOperationException("Locale scale data is invalid.", ex);
} Prevention
- Author locale scale definitions in a clean divisibility chain (e.g. 20, 400, 8000 for base-20).
- Write a unit test that constructs each locale profile and parses a sample phrase at CI time.
- Regenerate locale source from YAML via the source generator after any scale change.
- Review scale values against the locale's numeral system documentation.
When it happens
Trigger: Parsing any number phrase on a linked-vigesimal locale whose TokenizedScales array has adjacent rows where the larger scale is not a multiple of the next smaller scale (e.g. scales [20, 6] instead of [20, 4]). The error fires when the parser computes the max count for a non-first scale during TryParseScale.
Common situations: Authoring or editing a locale YAML file with scale values that are not in a clean divisibility chain; a locale generator bug producing wrong scale values; copying scale definitions from a different vigesimal base (base-20 vs base-10) without recalculating; a regression after a locale refactor.
Related errors
- Linked-vigesimal parser profiles require positive scale valu
- Linked-vigesimal parser profiles require descending scales.
- A dedicated 71 spelling is required when specialSeventyOneEn
- Cannot extract negative prefix for culture '{culture.Name}'
- '{path}.units' must explicitly define '{unitName}'.
AI-assisted analysis of Humanizr/Humanizer@ffc2b77c0f (2026-08-13).
Data as JSON: /api/errors/1bfa32b639d9794c.
Report an issue: GitHub.