{"record":{"id":"a5851824d43e6047","repo":"iOfficeAI/OfficeCLI","slug":"invalid-range-spec-expected-one-or-more-star","errorCode":null,"errorMessage":"Invalid range '{spec}'. Expected one or more 'start:end' ranges (e.g. '6:11' or '6:11,20:25').","messagePattern":"Invalid range '(.+?)'\\. Expected one or more 'start:end' ranges \\(e\\.g\\. '6:11' or '6:11,20:25'\\)\\.","errorType":"exception","errorClass":"ArgumentException","httpStatus":null,"severity":"error","filePath":"src/officecli/Core/ParseHelpers.cs","lineNumber":466,"sourceCode":"    /// Parse a comma-separated list of \"start:end\" character ranges into 0-based,\n    /// half-open offset pairs (e.g. \"6:11,20:25\" → [(6,11),(20,25)]). A single\n    /// range needs no comma. This lets one range= command target several disjoint\n    /// spans — the same shape find's format path produces from multiple matches —\n    /// so range is a complete addressing alternative wherever the caller already\n    /// knows the offsets. Order is preserved; the caller applies them (format-only\n    /// run splitting does not shift character offsets, so any order is safe).\n    /// </summary>\n    public static List<(int Start, int End)> ParseCharRanges(string spec)\n    {\n        var result = new List<(int Start, int End)>();\n        foreach (var seg in spec.Split(','))\n        {\n            var trimmed = seg.Trim();\n            if (trimmed.Length == 0) continue;\n            result.Add(ParseCharRange(trimmed));\n        }\n        if (result.Count == 0)\n            throw new ArgumentException(\n                $\"Invalid range '{spec}'. Expected one or more 'start:end' ranges \" +\n                \"(e.g. '6:11' or '6:11,20:25').\");\n        // Normalize to ascending position order (by Start, then End) regardless of\n        // the order the caller listed them. This matches find, whose matches are\n        // inherently position-ordered, and lets a future text-mutating path process\n        // ranges back-to-front (descending) so earlier offsets stay valid — the same\n        // reason ProcessFindInParagraph iterates its matches in reverse.\n        result.Sort((a, b) => a.Start != b.Start ? a.Start.CompareTo(b.Start) : a.End.CompareTo(b.End));\n        return result;\n    }\n\n    /// <summary>\n    /// Safely parse a string as double, throwing ArgumentException with a clear message on failure.\n    /// </summary>\n    public static double SafeParseDouble(string value, string propertyName)\n    {\n        // NumberStyles.Float (NOT the default Float|AllowThousands) — matches every\n        // other numeric parser in this file. AllowThousands would silently strip a","sourceCodeStart":448,"sourceCodeEnd":484,"githubUrl":"https://github.com/iOfficeAI/OfficeCLI/blob/1ced45e900782c5083ed550ddf328ee974e425e7/src/officecli/Core/ParseHelpers.cs#L448-L484","documentation":"Thrown by ParseHelpers.ParseCharRanges when the comma-separated range spec yields zero valid ranges. The function splits on ',', trims, skips empty segments, and collects each via ParseCharRange; if nothing remains it rejects the spec. This guards against silently treating a malformed/empty range= as 'no ranges'.","triggerScenarios":"Passing range=\"\" , range=\",\" , range=\" , , \" (only empty segments), or range=\" : \" (a segment that is empty after trimming). Any of these leaves the result list empty.","commonSituations":"Passing an empty string when 'range' was unset but the key still present in a JSON/properties dict; trailing commas from programmatic list joining (e.g. string.Join(\",\", offsets) over an empty collection); whitespace-only values from copy-paste.","solutions":["Provide at least one valid 'start:end' pair, e.g. range=\"6:11\" or range=\"6:11,20:25\".","If the caller should treat an absent range as 'no-op', omit the range key entirely rather than passing an empty string.","When building the spec programmatically, filter out empty entries before joining."],"exampleFix":"// before\nrange=\"\"\n// after\nrange=\"6:11\"","handlingStrategy":"validation","validationCode":"// Ensure the spec is non-empty and contains at least one non-empty segment:\nstatic bool HasAtLeastOneRange(string spec)\n    => spec.Split(',').Any(s => !string.IsNullOrWhiteSpace(s));","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Omit the range key entirely when no range is intended instead of passing an empty string.","When building the spec from a collection, filter empty entries before string.Join.","Treat a whitespace-only range as user error, not as 'no-op'."],"tags":["parsing","input-validation","range","officecli"],"backgroundTag":null,"analyzedSha":"1ced45e900782c5083ed550ddf328ee974e425e7","analyzedAt":"2026-08-13T13:01:07.193Z","schemaVersion":2},"datasetVersion":"2026-08-13T14:17:21.547Z"}