Humanizr/Humanizer · error · InvalidOperationException

Scale-leading compound parser profiles require descending sc

Error message

Scale-leading compound parser profiles require descending scales where each larger scale is divisible by the next smaller scale.

What it means

ScaleLeadingCompoundWordsToNumberConverter.GetMaximumCountForScale throws this InvalidOperationException at line 298 when adjacent scale values are not strictly descending with the larger evenly divisible by the smaller. The scale-leading grammar multiplies counts against scales, so a non-divisible or non-descending ladder makes the math ambiguous. This is a runtime invariant check on profile data, so it indicates a misconfigured locale profile rather than bad user input.

Source

Thrown at src/Humanizer/Localisation/WordsToNumber/ScaleLeadingCompoundWordsToNumberConverter.cs:298

            return true;
        }

        parsedValue = default;
        return false;
    }

    ulong GetMaximumCountForScale(int scaleIndex)
    {
        if (scaleIndex == 0)
        {
            return ulong.MaxValue / (ulong)profile.Scales[scaleIndex].Value;
        }

        var previous = (ulong)profile.Scales[scaleIndex - 1].Value;
        var current = (ulong)profile.Scales[scaleIndex].Value;
        if (previous <= current || previous % current != 0)
        {
            throw new InvalidOperationException("Scale-leading compound parser profiles require descending scales where each larger scale is divisible by the next smaller scale.");
        }

        return previous / current - 1UL;
    }

    bool TryParseOrdinalCandidate(string originalCandidate, string strippedCandidate, bool negative, out long parsedValue)
    {
        if (TryParseOrdinalCandidate(originalCandidate, negative, out parsedValue))
        {
            return true;
        }

        return !string.Equals(originalCandidate, strippedCandidate, StringComparison.Ordinal) &&
            TryParseOrdinalCandidate(strippedCandidate, negative, out parsedValue);
    }

    bool TryParseOrdinalCandidate(string candidate, bool negative, out long parsedValue)
    {

View on GitHub (pinned to ffc2b77c0f)

Solutions

  1. Fix the locale's scale ladder so each scale is strictly smaller than the previous and the previous is divisible by it.
  2. Run the locale's words-to-number tests to confirm the corrected ladder parses correctly.
  3. If you need non-divisible scales, that grammar is not supported here; use a different converter family.

Example fix

// before (profile scales)
Scales: [ 1000000, 1000, 100, 60 ]  // 100 % 60 != 0 -> throws
// after
Scales: [ 1000000, 1000, 100 ]      // 1000 % 100 == 0 -> ok
Defensive patterns

Strategy: validation

Validate before calling

// Authoring guard: verify the scale ladder is strictly descending & divisible.
static bool ScalesAreValid(long[] scales)
{
    for (int i = 0; i < scales.Length; i++)
    {
        if (scales[i] <= 0) return false;
        if (i > 0 && (scales[i - 1] <= scales[i] || scales[i - 1] % scales[i] != 0))
            return false;
    }
    return true;
}

Prevention

When it happens

Trigger: A ScaleLeadingCompoundWordsToNumberProfile whose Scales array is not strictly descending or not cleanly divisible (e.g. [..., 1000, 100, 60] where 100 is not divisible by 60). It fires during a parse call that reaches that scale index.

Common situations: Editing a scale-leading locale's scale ladder; a typo making a larger scale smaller than the next; using mixed radix (e.g. 60/24 time-like scales) that violates divisibility; generator output from malformed YAML.

Related errors


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