{"record":{"id":"58cf3f7cba595a0e","repo":"elastic/elasticsearch","slug":"q-should-be-in-0-1-got-q-58cf3f","errorCode":null,"errorMessage":"q should be in [0,1], got ${q}","messagePattern":"q should be in \\[0,1\\], got (.+?)","errorType":"validation","errorClass":"IllegalArgumentException","httpStatus":null,"severity":"error","filePath":"libs/tdigest/src/main/java/org/elasticsearch/tdigest/MergingDigest.java","lineNumber":487,"sourceCode":"                weightSoFar += weight.get(i);\n                left = right;\n            }\n\n            // for the last element, assume right width is same as left\n            int lastOffset = lastUsedCell - 1;\n            double right = (mean.get(lastOffset) - mean.get(lastOffset - 1)) / 2;\n            if (x < mean.get(lastOffset) + right) {\n                return (weightSoFar + weight.get(lastOffset) * interpolate(x, mean.get(lastOffset) - right, mean.get(lastOffset) + right))\n                    / size();\n            }\n            return 1;\n        }\n    }\n\n    @Override\n    public double quantile(double q) {\n        if (q < 0 || q > 1) {\n            throw new IllegalArgumentException(\"q should be in [0,1], got \" + q);\n        }\n        mergeNewValues();\n\n        if (lastUsedCell == 0) {\n            // no centroids means no data, no way to get a quantile\n            return Double.NaN;\n        } else if (lastUsedCell == 1) {\n            // with one data point, all quantiles lead to Rome\n            return mean.get(0);\n        }\n\n        // we know that there are at least two centroids now\n        int n = lastUsedCell;\n\n        // if values were stored in a sorted array, index would be the offset we are interested in\n        final double index = q * totalWeight;\n\n        // beyond the boundaries, we return min or max","sourceCodeStart":469,"sourceCodeEnd":505,"githubUrl":"https://github.com/elastic/elasticsearch/blob/db6a809a667c081ca1dc7500389d26975573215f/libs/tdigest/src/main/java/org/elasticsearch/tdigest/MergingDigest.java#L469-L505","documentation":"MergingDigest.quantile(q) is the MergingTDigest variant of the quantile lookup. It enforces the same [0,1] contract as AVLTreeDigest and throws IllegalArgumentException for any q outside it before merging new values and reading centroids. Behaviour for empty/single-centroid digests mirrors AVLTreeDigest.","triggerScenarios":"Calling mergingDigest.quantile(q) with q < 0, q > 1, or q = NaN. This variant first validates then calls mergeNewValues(), so the throw happens before any merging work.","commonSituations":"Same family of bugs as AVLTreeDigest: percent-vs-fraction confusion, unbounded ratios, NaN propagation, off-by-one in array indexing that produces the quantile argument.","solutions":["Pass q in [0,1]; convert percentages by dividing by 100","Clamp: Math.max(0.0, Math.min(1.0, q))","Guard against NaN before calling"],"exampleFix":"// before\ndouble v = digest.quantile(p); // p = 1.5\n// after\ndouble v = digest.quantile(Math.max(0.0, Math.min(1.0, p)));","handlingStrategy":"validation","validationCode":"if (Double.isNaN(q) || q < 0.0 || q > 1.0) {\n    throw new IllegalArgumentException(\"quantile out of range: \" + q);\n}\nreturn digest.quantile(q);","typeGuard":"static boolean isValidQuantile(double q) {\n    return !Double.isNaN(q) && q >= 0.0 && q <= 1.0;\n}","tryCatchPattern":"try { digest.quantile(q); }\ncatch (IllegalArgumentException e) { /* log and degrade */ }","preventionTips":["Share a single clamp helper across AVLTreeDigest and MergingDigest call sites","Treat percentile inputs as untrusted","Reject NaN upstream"],"tags":["tdigest","quantile","math","input-validation"],"backgroundTag":null,"analyzedSha":"db6a809a667c081ca1dc7500389d26975573215f","analyzedAt":"2026-08-12T01:39:14.192Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}