{"record":{"id":"1bfa32b639d9794c","repo":"Humanizr/Humanizer","slug":"linked-vigesimal-parser-profiles-require-descendin","errorCode":null,"errorMessage":"Linked-vigesimal parser profiles require descending scales where each larger scale is divisible by the next smaller scale.","messagePattern":"Linked-vigesimal parser profiles require descending scales where each larger scale is divisible by the next smaller scale\\.","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"critical","filePath":"src/Humanizer/Localisation/WordsToNumber/LinkedVigesimalWordsToNumberConverter.cs","lineNumber":203,"sourceCode":"        }\n\n        value = firstValue;\n        next = afterFirst;\n        return true;\n    }\n\n    ulong GetMaximumCountForScale(int scaleIndex)\n    {\n        if (scaleIndex == 0)\n        {\n            return ulong.MaxValue / (ulong)profile.TokenizedScales[scaleIndex].Value;\n        }\n\n        var previous = (ulong)profile.TokenizedScales[scaleIndex - 1].Value;\n        var current = (ulong)profile.TokenizedScales[scaleIndex].Value;\n        if (previous <= current || previous % current != 0)\n        {\n            throw new InvalidOperationException(\"Linked-vigesimal parser profiles require descending scales where each larger scale is divisible by the next smaller scale.\");\n        }\n\n        return previous / current - 1UL;\n    }\n\n    int FindScaleCountEnd(string[] tokens, int start, int end, ulong parentScaleValue)\n    {\n        for (var index = start; index < end; index++)\n        {\n            for (var scaleIndex = 0; scaleIndex < profile.TokenizedScales.Length; scaleIndex++)\n            {\n                var scale = profile.TokenizedScales[scaleIndex];\n                if ((ulong)scale.Value < parentScaleValue &&\n                    (TryMatchTokenPhrase(tokens, index, end, scale.NameTokens, out _) ||\n                     TryMatchTokenPhrase(tokens, index, end, scale.NameWithRemainderTokens, out _)))\n                {\n                    return index;\n                }","sourceCodeStart":185,"sourceCodeEnd":221,"githubUrl":"https://github.com/Humanizr/Humanizer/blob/ffc2b77c0f30d2fb176875841424379319d0ae9b/src/Humanizer/Localisation/WordsToNumber/LinkedVigesimalWordsToNumberConverter.cs#L185-L221","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before (locale YAML scales)\n//  - value: 20\n//  - value: 15   # not a divisor of 20\n\n// after\n//  - value: 20\n//  - value: 4    # 20 / 4 = 5, clean division","handlingStrategy":"validation","validationCode":"// For locale authors: validate scale divisibility before registration\nstatic bool ScalesAreDivisible(long[] scales)\n{\n    for (var i = 1; i < scales.Length; i++)\n    {\n        if (scales[i - 1] <= scales[i] || scales[i - 1] % scales[i] != 0)\n            return false;\n    }\n    return true;\n}","typeGuard":null,"tryCatchPattern":"try\n{\n    return words.ToNumber(culture);\n}\ncatch (InvalidOperationException ex) when (ex.Message.Contains(\"divisible by the next smaller scale\"))\n{\n    // Locale scale data is corrupt; the profile itself is misconfigured\n    throw new InvalidOperationException(\"Locale scale data is invalid.\", ex);\n}","preventionTips":["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."],"tags":["invalidoperation","locale-data","linked-vigesimal","scale-validation","configuration"],"backgroundTag":null,"analyzedSha":"ffc2b77c0f30d2fb176875841424379319d0ae9b","analyzedAt":"2026-08-13T21:42:34.584Z","schemaVersion":2},"datasetVersion":"2026-08-14T00:17:13.853Z"}