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

  1. 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).
  2. Regenerate the locale source from corrected YAML using the project's source generator.
  3. Write a unit test that constructs the profile and parses a sample phrase to catch the invariant at CI time.
  4. 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

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


AI-assisted analysis of Humanizr/Humanizer@ffc2b77c0f (2026-08-13). Data as JSON: /api/errors/1bfa32b639d9794c. Report an issue: GitHub.