{"record":{"id":"8bbf561eb7f55bea","repo":"gastownhall/beads","slug":"invalid-number-q","errorCode":null,"errorMessage":"invalid number %q","messagePattern":"invalid number %q","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/formula/range.go","lineNumber":149,"sourceCode":"\n\tfor i < len(expr) {\n\t\tch := expr[i]\n\n\t\t// Skip whitespace\n\t\tif unicode.IsSpace(rune(ch)) {\n\t\t\ti++\n\t\t\tcontinue\n\t\t}\n\n\t\t// Number\n\t\tif unicode.IsDigit(rune(ch)) {\n\t\t\tj := i\n\t\t\tfor j < len(expr) && (unicode.IsDigit(rune(expr[j])) || expr[j] == '.') {\n\t\t\t\tj++\n\t\t\t}\n\t\t\tval, err := strconv.ParseFloat(expr[i:j], 64)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"invalid number %q\", expr[i:j])\n\t\t\t}\n\t\t\ttokens = append(tokens, token{tokNumber, val})\n\t\t\ti = j\n\t\t\tcontinue\n\t\t}\n\n\t\t// Operators\n\t\tswitch ch {\n\t\tcase '+':\n\t\t\ttokens = append(tokens, token{tokPlus, 0})\n\t\tcase '-':\n\t\t\t// Could be unary minus or subtraction\n\t\t\t// If previous token is not a number or right paren, it's unary\n\t\t\tif len(tokens) == 0 || (tokens[len(tokens)-1].typ != tokNumber && tokens[len(tokens)-1].typ != tokRParen) {\n\t\t\t\t// Unary minus: parse the number with the minus\n\t\t\t\tj := i + 1\n\t\t\t\tfor j < len(expr) && (unicode.IsDigit(rune(expr[j])) || expr[j] == '.') {\n\t\t\t\t\tj++","sourceCodeStart":131,"sourceCodeEnd":167,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/formula/range.go#L131-L167","documentation":"tokenize() scans a numeric literal in a range expression and hands the slice to strconv.ParseFloat. If the slice cannot be parsed as a float64 (e.g. multiple dots like '1.2.3'), the tokenizer fails with this error. It is the low-level lexical validation of number literals in range start/end expressions.","triggerScenarios":"Calling EvaluateExpr or ValidateRange with an expression containing a malformed numeric literal that the digit/dot scan captures, such as '1.2.3..10' — the scan grabs '1.2.3' (digits and dots) and ParseFloat rejects it.","commonSituations":"Typos in loop specs like '..' inside a number, pasted expressions with thousands separators ('1,000' won't scan here but '1.0.0' will), or template placeholders that expand into malformed numbers.","solutions":["Fix the numeric literal so it has at most one decimal point (e.g. '1.2..10')","Run ValidateRange on the expression before evaluation to get the same diagnostic early","If the value comes from a variable, check the substituted value is a plain number"],"exampleFix":"// before\nrange 1.2.3..10 { ... }\n// after\nrange 1.23..10 { ... }","handlingStrategy":"validation","validationCode":"re := /^[0-9]+(\\.[0-9]+)?$/\nfor _, part := range strings.Split(expr, \"..\") {\n    // after variable substitution, each numeric literal should match\n    _ = re\n}\nValidateRange(expr) // run before EvaluateExpr to surface this early","typeGuard":null,"tryCatchPattern":"tokens, err := tokenize(expr)\nif err != nil && strings.Contains(err.Error(), \"invalid number\") {\n    // fall back to a default range or report a user-facing config error\n}","preventionTips":["Run ValidateRange on every range expression before evaluation","Never build numeric literals by string concatenation of user input","Reject values with more than one '.' during config load"],"tags":["formula","tokenizer","numeric-literal","parsing"],"backgroundTag":"invalid-number-literal","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}