This commit is contained in:
IRainman 2026-07-05 20:14:17 +03:00
commit eef7133edc
25 changed files with 1913 additions and 243 deletions

64
.github/workflows/pages.yml vendored Normal file
View File

@ -0,0 +1,64 @@
name: Deploy GitHub Pages
on:
push:
branches: [main]
paths:
- "docs/**"
- ".github/workflows/pages.yml"
- "CMakeLists.txt"
release:
types: [published]
workflow_dispatch:
permissions:
contents: write # Changed: need write to push to gh-pages
pages: read # No longer needed for deploy-pages
id-token: none
concurrency:
group: "pages"
cancel-in-progress: false
jobs:
deploy:
name: Deploy to gh-pages
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
fetch-tags: true
- name: Resolve latest release version
id: version
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
tag="$(gh release view --repo ${{ github.repository }} --json tagName --jq .tagName 2>/dev/null || true)"
if [ -z "${tag:-}" ]; then
tag="$(git tag --sort=-v:refname | grep -E '^v?[0-9]+\.[0-9]+\.[0-9]+$' | head -n 1 || true)"
fi
if [ -z "${tag:-}" ]; then
tag="v$(grep -E 'project\(fast_float VERSION' CMakeLists.txt | sed -E 's/.*VERSION ([0-9.]+).*/\1/')"
fi
version="${tag#v}"
echo "tag=${tag}" >> "$GITHUB_OUTPUT"
echo "version=${version}" >> "$GITHUB_OUTPUT"
echo "Resolved version: ${version}"
- name: Substitute version into HTML
run: |
version='${{ steps.version.outputs.version }}'
find docs -type f \( -name '*.html' -o -name '*.md' -o -name '*.css' -o -name '*.js' \) \
-exec sed -i "s/{{VERSION}}/${version}/g" {} +
- name: Deploy to gh-pages branch
uses: JamesIves/github-pages-deploy-action@v4
with:
branch: gh-pages
folder: docs
clean: true
commit-message: "Deploy docs for version ${{ steps.version.outputs.version }}"

View File

@ -10,8 +10,8 @@ jobs:
fail-fast: false
matrix:
include:
- {gen: Visual Studio 17 2022, arch: ARM64, cfg: Release}
- {gen: Visual Studio 17 2022, arch: ARM64, cfg: Debug}
- {gen: Visual Studio 18 2026, arch: ARM64, cfg: Release}
- {gen: Visual Studio 18 2026, arch: ARM64, cfg: Debug}
steps:
- name: checkout
uses: actions/checkout@v6.0.2

View File

@ -10,10 +10,10 @@ jobs:
fail-fast: false
matrix:
include:
- {gen: Visual Studio 17 2022, arch: Win32, cfg: Release}
#- {gen: Visual Studio 17 2022, arch: Win32, cfg: Debug}
- {gen: Visual Studio 17 2022, arch: x64, cfg: Release}
- {gen: Visual Studio 17 2022, arch: x64, cfg: Debug}
- {gen: Visual Studio 18 2026, arch: Win32, cfg: Release}
#- {gen: Visual Studio 18 2026, arch: Win32, cfg: Debug}
- {gen: Visual Studio 18 2026, arch: x64, cfg: Release}
- {gen: Visual Studio 18 2026, arch: x64, cfg: Debug}
steps:
- name: checkout
uses: actions/checkout@v6.0.2

View File

@ -10,10 +10,10 @@ jobs:
fail-fast: false
matrix:
include:
- {gen: Visual Studio 17 2022, arch: Win32, cfg: Release}
- {gen: Visual Studio 17 2022, arch: Win32, cfg: Debug}
- {gen: Visual Studio 17 2022, arch: x64, cfg: Release}
- {gen: Visual Studio 17 2022, arch: x64, cfg: Debug}
- {gen: Visual Studio 18 2026, arch: Win32, cfg: Release}
- {gen: Visual Studio 18 2026, arch: Win32, cfg: Debug}
- {gen: Visual Studio 18 2026, arch: x64, cfg: Release}
- {gen: Visual Studio 18 2026, arch: x64, cfg: Debug}
steps:
- name: checkout
uses: actions/checkout@v6.0.2

View File

@ -10,10 +10,10 @@ jobs:
fail-fast: false
matrix:
include:
- {gen: Visual Studio 17 2022, arch: Win32, cfg: Release}
- {gen: Visual Studio 17 2022, arch: Win32, cfg: Debug}
- {gen: Visual Studio 17 2022, arch: x64, cfg: Release}
- {gen: Visual Studio 17 2022, arch: x64, cfg: Debug}
- {gen: Visual Studio 18 2026, arch: Win32, cfg: Release}
- {gen: Visual Studio 18 2026, arch: Win32, cfg: Debug}
- {gen: Visual Studio 18 2026, arch: x64, cfg: Release}
- {gen: Visual Studio 18 2026, arch: x64, cfg: Debug}
steps:
- name: checkout
uses: actions/checkout@v6.0.2

View File

@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 3.14)
project(fast_float VERSION 8.2.5 LANGUAGES CXX)
project(fast_float VERSION 8.2.10 LANGUAGES CXX)
set(FASTFLOAT_CXX_STANDARD 11 CACHE STRING "the C++ standard to use for fastfloat")
set(CMAKE_CXX_STANDARD ${FASTFLOAT_CXX_STANDARD})
option(FASTFLOAT_TEST "Enable tests" OFF)

View File

@ -548,7 +548,6 @@ The fast_float library is part of:
* [WebKit](https://github.com/WebKit/WebKit), the engine behind Safari (Apple's
web browser),
* [DuckDB](https://duckdb.org),
* [Redis](https://github.com/redis/redis) and [Valkey](https://github.com/valkey-io/valkey),
* [Apache Arrow](https://github.com/apache/arrow/pull/8494) where it multiplied
the number parsing speed by two or three times,
* [Google Jsonnet](https://github.com/google/jsonnet),
@ -562,15 +561,6 @@ system](https://github.com/SerenityOS/serenity/commit/53b7f5e6a11e663c83df8030c3
has a derived implementation that is inherited by the [Ladybird
Browser](https://github.com/LadybirdBrowser/ladybird).
The fast_float library provides a performance similar to that of the
[fast_double_parser](https://github.com/lemire/fast_double_parser) library but
using an updated algorithm reworked from the ground up, and while offering an
API more in line with the expectations of C++ programmers. The
fast_double_parser library is part of the [Microsoft LightGBM machine-learning
framework](https://github.com/microsoft/LightGBM).
Packages
------
@ -648,7 +638,7 @@ sufficiently recent version of CMake (3.11 or better at least):
FetchContent_Declare(
fast_float
GIT_REPOSITORY https://github.com/fastfloat/fast_float.git
GIT_TAG tags/v8.2.5
GIT_TAG tags/v8.2.10
GIT_SHALLOW TRUE)
FetchContent_MakeAvailable(fast_float)
@ -664,7 +654,7 @@ You may also use [CPM](https://github.com/cpm-cmake/CPM.cmake), like so:
CPMAddPackage(
NAME fast_float
GITHUB_REPOSITORY "fastfloat/fast_float"
GIT_TAG v8.2.5)
GIT_TAG v8.2.10)
```
## Using as single header
@ -676,7 +666,7 @@ if desired as described in the command line help.
You may directly download automatically generated single-header files:
<https://github.com/fastfloat/fast_float/releases/download/v8.2.5/fast_float.h>
<https://github.com/fastfloat/fast_float/releases/download/v8.2.10/fast_float.h>
## Benchmarking

0
docs/.nojekyll Normal file
View File

25
docs/README.md Normal file
View File

@ -0,0 +1,25 @@
# fast_float website
The source for <https://fastfloat.github.io/fast_float/>.
Static HTML, CSS, and a small JS file — no build step required. To preview
locally, serve this directory with any static file server, for example:
```bash
python3 -m http.server -d docs 8080
# then open http://localhost:8080/
```
## How the version number stays current
The displayed release version is kept fresh by two independent mechanisms:
1. **Build-time substitution.** The `Deploy GitHub Pages` workflow
(`.github/workflows/pages.yml`) replaces every occurrence of the literal
`{{VERSION}}` in `index.html` with the latest GitHub release tag before
publishing. The workflow runs on every push to `main` that touches
`docs/**`, on every published release, and can be dispatched manually.
2. **Client-side refresh.** `assets/app.js` queries the GitHub Releases API
on page load and overwrites any element marked with `data-version`. This
means visitors see the very latest tag even between deploys.

116
docs/assets/app.js Normal file
View File

@ -0,0 +1,116 @@
(function () {
"use strict";
// ---------- Theme toggle ----------
const root = document.documentElement;
const storedTheme = localStorage.getItem("ff-theme");
if (storedTheme === "light" || storedTheme === "dark") {
root.setAttribute("data-theme", storedTheme);
}
const toggle = document.getElementById("theme-toggle");
if (toggle) {
toggle.addEventListener("click", function () {
const current =
root.getAttribute("data-theme") ||
(window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light");
const next = current === "dark" ? "light" : "dark";
root.setAttribute("data-theme", next);
localStorage.setItem("ff-theme", next);
});
}
// ---------- Copy-to-clipboard on code blocks ----------
document.querySelectorAll("pre > code").forEach(function (code) {
const pre = code.parentElement;
if (!pre || pre.querySelector(".copy-btn")) return;
const btn = document.createElement("button");
btn.className = "copy-btn";
btn.type = "button";
btn.textContent = "Copy";
btn.setAttribute("aria-label", "Copy code to clipboard");
btn.addEventListener("click", function () {
const text = code.innerText;
const done = function () {
btn.textContent = "Copied";
btn.classList.add("copied");
setTimeout(function () {
btn.textContent = "Copy";
btn.classList.remove("copied");
}, 1400);
};
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(done, function () {
fallbackCopy(text);
done();
});
} else {
fallbackCopy(text);
done();
}
});
pre.appendChild(btn);
});
function fallbackCopy(text) {
const ta = document.createElement("textarea");
ta.value = text;
ta.style.position = "fixed";
ta.style.opacity = "0";
document.body.appendChild(ta);
ta.select();
try { document.execCommand("copy"); } catch (e) { /* ignore */ }
document.body.removeChild(ta);
}
// ---------- Live version refresh from GitHub Releases ----------
// The build-time workflow substitutes {{VERSION}} in the HTML; this
// additionally refreshes the displayed version in the browser so the
// site always reflects the very latest release without redeploying.
const versionNodes = document.querySelectorAll("[data-version]");
const downloadLink = document.getElementById("download-latest");
if (versionNodes.length === 0) return;
// Don't refetch more than once per hour per visitor.
const CACHE_KEY = "ff-latest-release";
const CACHE_TTL = 60 * 60 * 1000;
let cached = null;
try {
const raw = localStorage.getItem(CACHE_KEY);
if (raw) {
const parsed = JSON.parse(raw);
if (parsed && parsed.ts && Date.now() - parsed.ts < CACHE_TTL) {
cached = parsed;
}
}
} catch (e) { /* ignore */ }
if (cached && cached.tag) {
applyVersion(cached.tag, cached.url);
} else {
fetch("https://api.github.com/repos/fastfloat/fast_float/releases/latest", {
headers: { Accept: "application/vnd.github+json" },
})
.then(function (r) { return r.ok ? r.json() : null; })
.then(function (data) {
if (!data || !data.tag_name) return;
try {
localStorage.setItem(
CACHE_KEY,
JSON.stringify({ ts: Date.now(), tag: data.tag_name, url: data.html_url })
);
} catch (e) { /* ignore */ }
applyVersion(data.tag_name, data.html_url);
})
.catch(function () { /* offline / rate limited — keep build-time value */ });
}
function applyVersion(tag, url) {
const clean = tag.replace(/^v/, "");
versionNodes.forEach(function (el) {
// Preserve a leading "v" if the original text used one.
const wasV = (el.textContent || "").trim().startsWith("v");
el.textContent = (wasV ? "v" : "") + clean;
});
if (downloadLink && url) downloadLink.href = url;
}
})();

15
docs/assets/logo.svg Normal file
View File

@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
<defs>
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#2563eb"/>
<stop offset="100%" stop-color="#0ea5e9"/>
</linearGradient>
</defs>
<rect x="2" y="2" width="60" height="60" rx="14" fill="url(#g)"/>
<path d="M16 44 L28 20 L36 36 L44 28 L52 44"
fill="none" stroke="#ffffff" stroke-width="4"
stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="28" cy="20" r="2.5" fill="#ffffff"/>
<circle cx="36" cy="36" r="2.5" fill="#ffffff"/>
<circle cx="44" cy="28" r="2.5" fill="#ffffff"/>
</svg>

After

Width:  |  Height:  |  Size: 663 B

454
docs/assets/style.css Normal file
View File

@ -0,0 +1,454 @@
:root {
--bg: #ffffff;
--bg-alt: #f7f8fb;
--surface: #ffffff;
--text: #0f172a;
--text-muted: #475569;
--border: #e2e8f0;
--accent: #2563eb;
--accent-strong: #1d4ed8;
--accent-soft: rgba(37, 99, 235, 0.10);
--grad-from: #2563eb;
--grad-to: #0ea5e9;
--code-bg: #0f172a;
--code-text: #e2e8f0;
--shadow: 0 1px 2px rgba(15, 23, 42, 0.04), 0 8px 24px rgba(15, 23, 42, 0.06);
--radius: 12px;
--radius-sm: 8px;
--maxw: 1100px;
}
:root[data-theme="dark"] {
--bg: #0b1020;
--bg-alt: #0f172a;
--surface: #121a2e;
--text: #e6edf7;
--text-muted: #94a3b8;
--border: #1f2a44;
--accent: #60a5fa;
--accent-strong: #93c5fd;
--accent-soft: rgba(96, 165, 250, 0.14);
--grad-from: #60a5fa;
--grad-to: #22d3ee;
--code-bg: #060a17;
--code-text: #e6edf7;
--shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 8px 24px rgba(0, 0, 0, 0.35);
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--bg: #0b1020;
--bg-alt: #0f172a;
--surface: #121a2e;
--text: #e6edf7;
--text-muted: #94a3b8;
--border: #1f2a44;
--accent: #60a5fa;
--accent-strong: #93c5fd;
--accent-soft: rgba(96, 165, 250, 0.14);
--grad-from: #60a5fa;
--grad-to: #22d3ee;
--code-bg: #060a17;
--code-text: #e6edf7;
--shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 8px 24px rgba(0, 0, 0, 0.35);
}
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
font-size: 16px;
line-height: 1.6;
color: var(--text);
background: var(--bg);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }
code, pre {
font-family: "SF Mono", Menlo, Consolas, "Liberation Mono", "Roboto Mono", monospace;
font-size: 0.92em;
}
:not(pre) > code {
background: var(--accent-soft);
color: var(--accent-strong);
padding: 0.12em 0.4em;
border-radius: 4px;
font-size: 0.88em;
}
pre {
background: var(--code-bg);
color: var(--code-text);
padding: 1.1rem 1.2rem;
border-radius: var(--radius);
overflow-x: auto;
margin: 1rem 0 1.5rem;
position: relative;
box-shadow: var(--shadow);
font-size: 0.88rem;
line-height: 1.55;
}
pre code { background: transparent; color: inherit; padding: 0; }
/* highlight.js applies its own background — keep our pre styling */
pre code.hljs { background: transparent; padding: 0; }
.copy-btn {
position: absolute;
top: 8px;
right: 8px;
background: rgba(255, 255, 255, 0.08);
color: var(--code-text);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 6px;
padding: 4px 10px;
font-size: 0.75rem;
cursor: pointer;
opacity: 0;
transition: opacity 0.15s ease, background 0.15s ease;
}
pre:hover .copy-btn { opacity: 1; }
.copy-btn:hover { background: rgba(255, 255, 255, 0.15); }
.copy-btn.copied { background: rgba(34, 197, 94, 0.25); border-color: rgba(34, 197, 94, 0.5); }
.container {
max-width: var(--maxw);
margin: 0 auto;
padding: 0 1.25rem;
}
.skip {
position: absolute; left: -9999px;
background: var(--accent); color: white;
padding: 0.6rem 1rem; border-radius: 0 0 6px 0;
}
.skip:focus { left: 0; top: 0; }
/* Header */
.site-header {
position: sticky;
top: 0;
z-index: 50;
backdrop-filter: saturate(180%) blur(12px);
-webkit-backdrop-filter: saturate(180%) blur(12px);
background: color-mix(in srgb, var(--bg) 80%, transparent);
border-bottom: 1px solid var(--border);
}
.nav {
display: flex;
align-items: center;
gap: 1.25rem;
height: 60px;
}
.brand {
display: inline-flex;
align-items: center;
gap: 0.55rem;
font-weight: 700;
color: var(--text);
text-decoration: none;
}
.brand img { display: block; }
.primary-nav {
display: flex;
gap: 1.2rem;
margin-left: 1rem;
flex: 1;
}
.primary-nav a {
color: var(--text-muted);
font-size: 0.92rem;
font-weight: 500;
}
.primary-nav a:hover { color: var(--text); text-decoration: none; }
.nav-actions { display: flex; align-items: center; gap: 0.5rem; }
.ghost-btn {
display: inline-flex; align-items: center; gap: 0.4rem;
padding: 0.45rem 0.8rem;
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text);
font-size: 0.9rem;
background: var(--surface);
transition: border-color 0.15s, background 0.15s;
}
.ghost-btn:hover { border-color: var(--accent); text-decoration: none; }
.theme-toggle {
width: 36px; height: 36px;
display: inline-flex; align-items: center; justify-content: center;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--surface);
color: var(--text);
cursor: pointer;
}
.theme-toggle .i-moon { display: none; }
.theme-toggle .i-sun { display: block; }
:root[data-theme="dark"] .theme-toggle .i-sun,
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) .theme-toggle .i-sun { display: none; }
:root:not([data-theme="light"]) .theme-toggle .i-moon { display: block; }
}
:root[data-theme="dark"] .theme-toggle .i-sun { display: none; }
:root[data-theme="dark"] .theme-toggle .i-moon { display: block; }
:root[data-theme="light"] .theme-toggle .i-sun { display: block; }
:root[data-theme="light"] .theme-toggle .i-moon { display: none; }
/* Hero */
.hero {
padding: 5rem 0 4rem;
background:
radial-gradient(1200px 480px at 50% -10%, var(--accent-soft), transparent 70%),
var(--bg);
border-bottom: 1px solid var(--border);
}
.eyebrow {
display: inline-block;
font-size: 0.78rem;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--accent);
background: var(--accent-soft);
padding: 0.3rem 0.7rem;
border-radius: 999px;
margin-bottom: 1.25rem;
}
.hero h1 {
font-size: clamp(2rem, 4.6vw, 3.6rem);
line-height: 1.1;
margin: 0 0 1.25rem;
font-weight: 800;
letter-spacing: -0.02em;
}
.grad {
background: linear-gradient(90deg, var(--grad-from), var(--grad-to));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.lede {
font-size: 1.15rem;
color: var(--text-muted);
max-width: 720px;
margin: 0 0 2rem;
}
.lede strong { color: var(--text); }
.cta-row { display: flex; flex-wrap: wrap; gap: 0.7rem; margin-bottom: 1.75rem; }
.btn {
display: inline-flex; align-items: center; gap: 0.5rem;
padding: 0.7rem 1.1rem;
border-radius: 10px;
font-weight: 600;
font-size: 0.95rem;
border: 1px solid var(--border);
background: var(--surface);
color: var(--text);
cursor: pointer;
transition: transform 0.05s ease, border-color 0.15s, background 0.15s;
}
.btn:hover { text-decoration: none; border-color: var(--accent); }
.btn:active { transform: translateY(1px); }
.btn.primary {
background: var(--accent);
border-color: var(--accent);
color: white;
box-shadow: 0 6px 20px rgba(37, 99, 235, 0.25);
}
.btn.primary:hover { background: var(--accent-strong); border-color: var(--accent-strong); }
.btn.ghost { background: transparent; }
.btn code { background: rgba(255, 255, 255, 0.18); color: inherit; padding: 0.1em 0.45em; border-radius: 4px; }
.btn:not(.primary) code { background: var(--accent-soft); color: var(--accent-strong); }
.hero-meta {
display: flex;
flex-wrap: wrap;
gap: 0.6rem 1rem;
align-items: center;
font-size: 0.9rem;
color: var(--text-muted);
}
.badge {
display: inline-flex; align-items: center; gap: 0.5rem;
padding: 0.3rem 0.7rem;
border: 1px solid var(--border);
border-radius: 999px;
background: var(--surface);
}
.badge code { background: transparent; color: var(--text); padding: 0; }
.badge .dot {
width: 8px; height: 8px; border-radius: 50%;
background: #22c55e;
box-shadow: 0 0 0 4px rgba(34, 197, 94, 0.18);
}
.meta-link { color: var(--text-muted); }
.meta-link:hover { color: var(--accent); }
/* Sections */
.section { padding: 4.5rem 0; }
.section.alt { background: var(--bg-alt); border-top: 1px solid var(--border); border-bottom: 1px solid var(--border); }
.section h2 {
font-size: clamp(1.6rem, 3vw, 2.2rem);
margin: 0 0 0.5rem;
letter-spacing: -0.01em;
}
.section h3 { margin: 1.75rem 0 0.6rem; font-size: 1.1rem; }
.section-lede { color: var(--text-muted); max-width: 720px; margin: 0 0 2rem; }
/* Grid cards */
.grid { display: grid; gap: 1rem; }
.grid.features { grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); }
.grid.install { grid-template-columns: 1fr; }
.card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1.4rem 1.4rem 1.5rem;
box-shadow: var(--shadow);
transition: transform 0.12s ease, border-color 0.15s;
}
.card:hover { transform: translateY(-2px); border-color: var(--accent); }
.card h3 { margin: 0 0 0.5rem; font-size: 1.05rem; }
.card p, .card ul { margin: 0; color: var(--text-muted); font-size: 0.95rem; }
.card pre { margin: 0.6rem 0 0; font-size: 0.82rem; }
.bullets { padding-left: 1.1rem; }
.bullets li { margin: 0.2rem 0; }
/* Benchmarks */
.bench {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1.5rem;
box-shadow: var(--shadow);
}
.bench-row {
display: grid;
grid-template-columns: 160px 1fr;
align-items: center;
gap: 0.9rem;
margin: 0.55rem 0;
}
.bench-label { font-weight: 600; color: var(--text); font-size: 0.95rem; }
.bench-bar {
background: var(--bg-alt);
border-radius: 6px;
overflow: hidden;
height: 28px;
position: relative;
}
.bench-fill {
display: inline-flex;
align-items: center;
height: 100%;
width: var(--w, 0%);
background: linear-gradient(90deg, color-mix(in srgb, var(--accent) 35%, transparent), color-mix(in srgb, var(--accent) 55%, transparent));
color: var(--text);
padding: 0 0.7rem;
border-radius: 6px;
font-variant-numeric: tabular-nums;
font-size: 0.85rem;
font-weight: 600;
white-space: nowrap;
}
.bench-fill.primary {
background: linear-gradient(90deg, var(--grad-from), var(--grad-to));
color: white;
}
@media (max-width: 600px) {
.bench-row { grid-template-columns: 1fr; gap: 0.3rem; }
}
.repro {
margin-top: 1.25rem;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
background: var(--surface);
padding: 0.4rem 1rem;
}
.repro summary {
cursor: pointer;
padding: 0.5rem 0;
font-weight: 600;
color: var(--text-muted);
}
.repro[open] summary { color: var(--text); }
.repro pre { margin: 0.5rem 0 0.75rem; }
/* Users list */
.users {
list-style: none;
padding: 0;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 0.6rem 1rem;
}
.users li {
padding: 0.7rem 0.9rem;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
background: var(--surface);
font-size: 0.92rem;
color: var(--text-muted);
}
.users li strong { color: var(--text); margin-right: 0.35rem; }
.aside { color: var(--text-muted); margin-top: 1.5rem; font-size: 0.92rem; }
.papers { list-style: none; padding: 0; }
.papers li {
padding: 1rem 1.2rem;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
background: var(--surface);
margin-bottom: 0.7rem;
}
/* Footer */
.site-footer {
background: var(--bg-alt);
border-top: 1px solid var(--border);
padding: 3rem 0 2rem;
color: var(--text-muted);
font-size: 0.92rem;
margin-top: 2rem;
}
.footer-grid {
display: grid;
grid-template-columns: 1.5fr 1fr 1fr;
gap: 2rem;
margin-bottom: 2rem;
}
.site-footer h4 { color: var(--text); font-size: 0.95rem; margin: 0 0 0.6rem; }
.site-footer ul { list-style: none; padding: 0; margin: 0; }
.site-footer li { margin: 0.25rem 0; }
.subfoot {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 1.5rem;
border-top: 1px solid var(--border);
flex-wrap: wrap;
gap: 0.5rem;
}
.subfoot code { background: var(--surface); color: var(--text); border: 1px solid var(--border); }
@media (max-width: 720px) {
.primary-nav { display: none; }
.footer-grid { grid-template-columns: 1fr; gap: 1.5rem; }
.hero { padding: 3rem 0 2.5rem; }
.section { padding: 3rem 0; }
}

349
docs/index.html Normal file
View File

@ -0,0 +1,349 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>fast_float — parse floating-point numbers at a gigabyte per second</title>
<meta name="description" content="A header-only C++ library for fast and exact parsing of floating-point and integer numbers. Used by GCC, Chromium, WebKit, LLVM, Apache Arrow, DuckDB, Redis, and more." />
<meta name="theme-color" content="#0f172a" />
<meta property="og:title" content="fast_float — parse floats at 1 GB/s" />
<meta property="og:description" content="A header-only C++ library for fast and exact floating-point and integer parsing." />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://fastfloat.github.io/fast_float/" />
<meta name="twitter:card" content="summary_large_image" />
<link rel="icon" type="image/svg+xml" href="assets/logo.svg" />
<link rel="stylesheet" href="assets/style.css" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.10.0/build/styles/github-dark.min.css" />
</head>
<body>
<a class="skip" href="#main">Skip to content</a>
<header class="site-header">
<div class="container nav">
<a class="brand" href="./">
<img src="assets/logo.svg" alt="" width="32" height="32" />
<span>fast_float</span>
</a>
<nav class="primary-nav" aria-label="Primary">
<a href="#features">Features</a>
<a href="#performance">Performance</a>
<a href="#quickstart">Quick start</a>
<a href="#users">Users</a>
<a href="#install">Install</a>
</nav>
<div class="nav-actions">
<a class="ghost-btn" href="https://github.com/fastfloat/fast_float" aria-label="GitHub repository">
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 .5C5.65.5.5 5.65.5 12c0 5.08 3.29 9.39 7.86 10.91.58.1.79-.25.79-.56v-2c-3.2.7-3.88-1.37-3.88-1.37-.52-1.33-1.27-1.69-1.27-1.69-1.04-.71.08-.7.08-.7 1.15.08 1.76 1.18 1.76 1.18 1.03 1.76 2.7 1.25 3.36.96.1-.74.4-1.25.73-1.54-2.55-.29-5.24-1.28-5.24-5.7 0-1.26.45-2.29 1.18-3.1-.12-.29-.51-1.46.11-3.05 0 0 .97-.31 3.18 1.18a11 11 0 0 1 5.78 0c2.2-1.49 3.17-1.18 3.17-1.18.62 1.59.23 2.76.11 3.05.74.81 1.18 1.84 1.18 3.1 0 4.43-2.69 5.4-5.25 5.69.41.36.78 1.07.78 2.16v3.2c0 .31.21.67.8.56A11.5 11.5 0 0 0 23.5 12C23.5 5.65 18.35.5 12 .5z"/></svg>
<span>GitHub</span>
</a>
<button class="theme-toggle" id="theme-toggle" aria-label="Toggle color theme" title="Toggle theme">
<svg class="i-sun" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/></svg>
<svg class="i-moon" width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
</button>
</div>
</div>
</header>
<main id="main">
<section class="hero">
<div class="container">
<span class="eyebrow">C++11 · header-only · triple-licensed</span>
<h1>Parse floating-point numbers <span class="grad">at a gigabyte per second</span>.</h1>
<p class="lede">
<strong>fast_float</strong> is a header-only C++ implementation of
<code>std::from_chars</code> for <code>float</code>, <code>double</code>, and integer types.
Exact IEEE rounding, no allocations, no exceptions — often <strong>many times faster</strong>
than your standard library.
</p>
<div class="cta-row">
<a class="btn primary" href="#quickstart">Get started</a>
<a class="btn" href="https://github.com/fastfloat/fast_float">View on GitHub</a>
<a class="btn ghost" href="https://github.com/fastfloat/fast_float/releases/latest" id="download-latest">
Download <code class="version-tag" data-version>v{{VERSION}}</code>
</a>
</div>
<div class="hero-meta">
<span class="badge" id="release-badge">
<span class="dot"></span> Latest release: <code data-version>v{{VERSION}}</code>
</span>
<a class="meta-link" href="https://github.com/fastfloat/fast_float/blob/main/LICENSE-APACHE">Apache 2.0</a>
<a class="meta-link" href="https://github.com/fastfloat/fast_float/blob/main/LICENSE-MIT">MIT</a>
<a class="meta-link" href="https://github.com/fastfloat/fast_float/blob/main/LICENSE-BOOST">Boost</a>
</div>
</div>
</section>
<section id="features" class="section">
<div class="container">
<h2>Why fast_float?</h2>
<p class="section-lede">A drop-in <code>from_chars</code> built for performance-critical code paths.</p>
<div class="grid features">
<article class="card">
<h3>Blazing fast</h3>
<p>Often <strong>4× faster</strong> than the best competitor and many times faster than typical standard-library implementations. Sustains <strong>1 GB/s</strong> on commodity hardware.</p>
</article>
<article class="card">
<h3>Exact rounding</h3>
<p>Returns the closest IEEE 754 <code>float</code> or <code>double</code> with round-to-nearest, ties-to-even — bit-for-bit correct.</p>
</article>
<article class="card">
<h3>Header-only</h3>
<p>Just drop in <code>fast_float.h</code> or use it via CMake, Conan, vcpkg, xmake, or Homebrew. Requires only C++11.</p>
</article>
<article class="card">
<h3>No surprises</h3>
<p>Does not allocate, does not throw, locale-independent. The interface mirrors C++17 <code>std::from_chars</code>.</p>
</article>
<article class="card">
<h3>Integers too</h3>
<p>Parses every standard integer type in bases 236, plus <code>bool</code>. The same fast, allocation-free interface.</p>
</article>
<article class="card">
<h3>Unicode &amp; formats</h3>
<p>UTF-8, UTF-16, and UTF-32 inputs. JSON, Fortran, and custom decimal separators via <code>from_chars_advanced</code>.</p>
</article>
<article class="card">
<h3>constexpr-ready</h3>
<p>In C++20, parse strings at compile time with <code>consteval</code> — zero runtime cost.</p>
</article>
<article class="card">
<h3>Portable</h3>
<p>Visual Studio, GCC, Clang, MSYS2. Linux, macOS, FreeBSD, Windows. x86-64, ARM, RISC-V, s390x. 32-bit and 64-bit.</p>
</article>
</div>
</div>
</section>
<section id="performance" class="section alt">
<div class="container">
<h2>Performance</h2>
<p class="section-lede">
Parsing random floating-point numbers, measured in megabytes per second
(higher is better). Source: project benchmark suite on a typical x86-64 box.
</p>
<div class="bench">
<div class="bench-row">
<span class="bench-label">fast_float</span>
<div class="bench-bar"><span class="bench-fill primary" style="--w: 100%">1042 MB/s</span></div>
</div>
<div class="bench-row">
<span class="bench-label">abseil</span>
<div class="bench-bar"><span class="bench-fill" style="--w: 41%">430 MB/s</span></div>
</div>
<div class="bench-row">
<span class="bench-label">netlib</span>
<div class="bench-bar"><span class="bench-fill" style="--w: 26%">271 MB/s</span></div>
</div>
<div class="bench-row">
<span class="bench-label">double-conversion</span>
<div class="bench-bar"><span class="bench-fill" style="--w: 22%">225 MB/s</span></div>
</div>
<div class="bench-row">
<span class="bench-label">strtod</span>
<div class="bench-bar"><span class="bench-fill" style="--w: 18%">191 MB/s</span></div>
</div>
</div>
<details class="repro">
<summary>Reproduce these numbers</summary>
<pre data-lang="bash"><code>cmake -B build -D FASTFLOAT_BENCHMARKS=ON
cmake --build build
./build/benchmarks/benchmark</code></pre>
</details>
</div>
</section>
<section id="quickstart" class="section">
<div class="container">
<h2>Quick start</h2>
<p class="section-lede">Parse a <code>double</code> from a string in three lines.</p>
<pre data-lang="cpp"><code>#include "fast_float/fast_float.h"
#include &lt;iostream&gt;
#include &lt;string&gt;
int main() {
std::string input = "3.1416 xyz ";
double result;
auto answer = fast_float::from_chars(input.data(),
input.data() + input.size(),
result);
if (answer.ec != std::errc()) {
std::cerr &lt;&lt; "parsing failure\n";
return EXIT_FAILURE;
}
std::cout &lt;&lt; "parsed the number " &lt;&lt; result &lt;&lt; '\n';
}</code></pre>
<h3>Integers in any base (236)</h3>
<pre data-lang="cpp"><code>uint64_t value;
std::string hex = "4f0cedc95a718c";
auto r = fast_float::from_chars(hex.data(), hex.data() + hex.size(), value, 16);
// value == 22250738585072012</code></pre>
<h3>UTF-16 input</h3>
<pre data-lang="cpp"><code>std::u16string input = u"3.1416 xyz ";
double result;
auto r = fast_float::from_chars(input.data(), input.data() + input.size(), result);</code></pre>
<h3>Comma as decimal separator, Fortran, or JSON</h3>
<pre data-lang="cpp"><code>// "3,1416" — French-style
fast_float::parse_options opts{fast_float::chars_format::general, ','};
fast_float::from_chars_advanced(s.data(), s.data() + s.size(), result, opts);
// "1d+4" — Fortran exponent
opts = {fast_float::chars_format::fortran};
// strict JSON per RFC 8259
opts = {fast_float::chars_format::json};</code></pre>
<h3>C++20: parse at compile time</h3>
<pre data-lang="cpp"><code>consteval double parse(std::string_view s) {
double v;
auto r = fast_float::from_chars(s.data(), s.data() + s.size(), v);
return r.ec == std::errc() ? v : -1.0;
}
constexpr double pi = parse("3.1415"); // computed at compile time</code></pre>
</div>
</section>
<section id="install" class="section alt">
<div class="container">
<h2>Install</h2>
<p class="section-lede">Pick the workflow that matches your project.</p>
<div class="grid install">
<article class="card">
<h3>CMake <code>FetchContent</code></h3>
<pre data-lang="cmake"><code>FetchContent_Declare(
fast_float
GIT_REPOSITORY https://github.com/fastfloat/fast_float.git
GIT_TAG tags/<span data-version>v{{VERSION}}</span>
GIT_SHALLOW TRUE)
FetchContent_MakeAvailable(fast_float)
target_link_libraries(myprogram PUBLIC fast_float)</code></pre>
</article>
<article class="card">
<h3>CPM</h3>
<pre data-lang="cmake"><code>CPMAddPackage(
NAME fast_float
GITHUB_REPOSITORY "fastfloat/fast_float"
GIT_TAG <span data-version>v{{VERSION}}</span>)</code></pre>
</article>
<article class="card">
<h3>Single header</h3>
<p>Download a pre-amalgamated header — no build system required.</p>
<pre data-lang="bash"><code>curl -LO https://github.com/fastfloat/fast_float/releases/download/<span data-version>v{{VERSION}}</span>/fast_float.h</code></pre>
</article>
<article class="card">
<h3>Package managers</h3>
<ul class="bullets">
<li><a href="https://conan.io/center/recipes/fast_float">Conan</a></li>
<li><a href="https://formulae.brew.sh/formula/fast_float">Homebrew</a><code>brew install fast_float</code></li>
<li><a href="https://xmake.io">xmake</a></li>
<li>Fedora — <code>dnf install fast_float-devel</code></li>
<li><a href="https://repology.org/project/fast-float/versions">More distributions</a></li>
</ul>
</article>
</div>
</div>
</section>
<section id="users" class="section">
<div class="container">
<h2>Trusted by</h2>
<p class="section-lede">fast_float ships inside compilers, browsers, databases, and more.</p>
<ul class="users">
<li><strong>GCC</strong> — backs <code>std::from_chars</code> since version 12</li>
<li><strong>Chromium</strong> — Chrome, Edge, and Opera</li>
<li><strong>WebKit</strong> — Safari</li>
<li><strong>Ladybird</strong> — independent browser engine</li>
<li><strong>DuckDB</strong> — in-process analytical database</li>
<li><strong>Apache Arrow</strong> — 23× faster number parsing</li>
<li><strong>ClickHouse</strong> — OLAP database</li>
<li><strong>MySQL</strong></li>
<li><strong>Boost.JSON</strong></li>
<li><strong>Blender</strong></li>
<li><strong>Google Jsonnet</strong></li>
</ul>
<p class="aside">
Ports and bindings exist for
<a href="https://github.com/aldanor/fast-float-rust/">Rust</a>,
<a href="https://github.com/wrandelshofer/FastDoubleParser">Java</a>,
<a href="https://github.com/CarlVerret/csFastFloat">C#</a>,
<a href="https://github.com/kolemannix/ffc.h">C</a>, and
<a href="https://github.com/eddelbuettel/rcppfastfloat">R</a>.
</p>
</div>
</section>
<section id="papers" class="section alt">
<div class="container">
<h2>The research behind it</h2>
<ul class="papers">
<li>
Daniel Lemire,
<a href="https://arxiv.org/abs/2101.11408"><strong>Number Parsing at a Gigabyte per Second</strong></a>.
<em>Software: Practice and Experience</em> 51(8), 2021.
</li>
<li>
Noble Mushtak, Daniel Lemire,
<a href="https://arxiv.org/abs/2212.06644"><strong>Fast Number Parsing Without Fallback</strong></a>.
<em>Software: Practice and Experience</em> 53(7), 2023.
</li>
</ul>
</div>
</section>
</main>
<footer class="site-footer">
<div class="container footer-grid">
<div>
<strong>fast_float</strong>
<p>Triple-licensed under Apache 2.0, MIT, and Boost. Use it however you like.</p>
</div>
<div>
<h4>Project</h4>
<ul>
<li><a href="https://github.com/fastfloat/fast_float">GitHub repository</a></li>
<li><a href="https://github.com/fastfloat/fast_float/releases">Releases</a></li>
<li><a href="https://github.com/fastfloat/fast_float/issues">Issue tracker</a></li>
<li><a href="https://github.com/fastfloat/fast_float/blob/main/SECURITY.md">Security policy</a></li>
</ul>
</div>
<div>
<h4>Learn more</h4>
<ul>
<li><a href="https://arxiv.org/abs/2101.11408">Number Parsing at a Gigabyte per Second</a></li>
<li><a href="https://www.youtube.com/watch?v=AVXgvlMeIm4">Go Systems 2020 talk</a></li>
<li><a href="https://github.com/fastfloat/fast_float/blob/main/README.md">README</a></li>
</ul>
</div>
</div>
<div class="container subfoot">
<span>Current release <code data-version>v{{VERSION}}</code></span>
<span>Maintained by the <a href="https://github.com/fastfloat">fast_float</a> contributors.</span>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.10.0/build/highlight.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.10.0/build/languages/cmake.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.10.0/build/languages/bash.min.js"></script>
<script>
// Promote each <pre data-lang="X"> to a Highlight.js language class
// on its inner <code>, then highlight everything.
document.querySelectorAll("pre[data-lang]").forEach(function (pre) {
var code = pre.querySelector("code");
if (code && !code.className) code.className = "language-" + pre.dataset.lang;
});
hljs.highlightAll();
</script>
<script src="assets/app.js"></script>
</body>
</html>

View File

@ -248,6 +248,21 @@ loop_parse_if_eight_digits(char const *&p, char const *const pend,
p)); // in rare cases, this will overflow, but that's ok
p += 8;
}
// Consume a remaining 4-7 digit run in a single SWAR step instead of
// byte-by-byte (reuses the existing 4-digit helpers). The parsed result is
// identical either way. Gated to clang: on gcc the extra 4-digit check
// regresses inputs whose remainder is shorter than 4 digits (it becomes pure
// overhead there); clang does not show that.
#if defined(__clang__)
if ((pend - p) >= 4) {
uint32_t const val4 = read4_to_u32(p);
if (is_made_of_four_digits_fast(val4)) {
i = i * 10000 +
parse_four_digits_unrolled(val4); // may overflow, that's ok
p += 4;
}
}
#endif
}
enum class parse_error : uint_fast8_t {
@ -304,10 +319,18 @@ report_parse_error(parsed_number_string_t<UC> &answer, UC const *p,
// Assuming that you use no more than 19 digits, this will
// parse an ASCII string.
//
// store_spans is a *runtime* flag (not a template parameter, deliberately: a
// template would create a second instantiation of this whole function and the
// extra icache pressure wipes out the gain). When false, the integer/fraction
// spans (read only by the rare digit_comp slow path) are not materialized,
// which keeps the fat parsed_number_string_t off the hot path. The caller
// re-parses with store_spans=true if the slow path is actually reached.
template <bool basic_json_fmt, typename UC>
fastfloat_really_inline FASTFLOAT_CONSTEXPR20 parsed_number_string_t<UC>
parse_number_string(UC const *p, UC const *pend,
parse_options_t<UC> const options) noexcept {
parse_options_t<UC> const options,
bool store_spans = true) noexcept {
parsed_number_string_t<UC> answer{};
// so dereference without checks
FASTFLOAT_ASSUME(p < pend);
@ -338,24 +361,54 @@ parse_number_string(UC const *p, UC const *pend,
}
}
#endif
auto const *const start_digits = p;
while ((p != pend) && is_integer(*p)) {
// a multiplication by 10 is cheaper than an arbitrary integer
// multiplication
answer.mantissa = static_cast<fast_float::am_mant_t>(
answer.mantissa * 10 +
static_cast<uint8_t>(
*p - UC('0'))); // might overflow, we will handle the overflow later
// Straight-line unroll of the integer-part scan: most integer parts are
// 1-5 digits, so peeling the first iterations eliminates the loop back-edge
// for the common case. Semantics are identical to the original `while` loop:
// i = 10*i + digit, advancing p.
if ((p != pend) && is_integer(*p)) {
answer.mantissa = static_cast<fast_float::am_mant_t>(*p - UC('0'));
++p;
if ((p != pend) && is_integer(*p)) {
answer.mantissa = static_cast<fast_float::am_mant_t>(
answer.mantissa * 10 +
static_cast<fast_float::am_mant_t>(*p - UC('0')));
++p;
if ((p != pend) && is_integer(*p)) {
answer.mantissa = static_cast<fast_float::am_mant_t>(
answer.mantissa * 10 +
static_cast<fast_float::am_mant_t>(*p - UC('0')));
++p;
if ((p != pend) && is_integer(*p)) {
answer.mantissa = static_cast<fast_float::am_mant_t>(
answer.mantissa * 10 +
static_cast<fast_float::am_mant_t>(*p - UC('0')));
++p;
if ((p != pend) && is_integer(*p)) {
answer.mantissa = static_cast<fast_float::am_mant_t>(
answer.mantissa * 10 +
static_cast<fast_float::am_mant_t>(*p - UC('0')));
++p;
while ((p != pend) && is_integer(*p)) {
// a multiplication by 10 is cheaper than an arbitrary integer
// multiplication
answer.mantissa = static_cast<fast_float::am_mant_t>(
answer.mantissa * 10 +
static_cast<fast_float::am_mant_t>(
*p - UC('0'))); // might overflow, handled later
++p;
}
}
}
}
}
}
UC const *const end_of_integer_part = p;
int64_t digit_count = int64_t(end_of_integer_part - start_digits);
if (store_spans) {
answer.integer = span<UC const>(start_digits, size_t(digit_count));
}
auto const *const end_of_integer_part = p;
auto digit_count = static_cast<am_digits>(end_of_integer_part - start_digits);
answer.integer = span<UC const>(start_digits, digit_count);
// We have now parsed the integer part of the mantissa.
#ifndef FASTFLOAT_ONLY_POSITIVE_C_NUMBER_WO_INF_NAN
FASTFLOAT_IF_CONSTEXPR17(basic_json_fmt) {
// at least 1 digit in integer part, without leading zeros
@ -371,7 +424,8 @@ parse_number_string(UC const *p, UC const *pend,
#endif
// We can now parse the fraction part of the mantissa.
if ((p != pend) && (*p == options.decimal_point)) {
bool const has_decimal_point = (p != pend) && (*p == options.decimal_point);
if (has_decimal_point) {
++p;
auto const *const before = p;
// can occur at most twice without overflowing, but let it occur more, since
@ -380,14 +434,16 @@ parse_number_string(UC const *p, UC const *pend,
while ((p != pend) && is_integer(*p)) {
auto const digit = uint8_t(*p - UC('0'));
++p;
answer.mantissa = static_cast<fast_float::am_mant_t>(
answer.mantissa * 10 +
digit); // in rare cases, this will overflow, but that's ok
++p;
}
answer.exponent = static_cast<am_pow_t>(before - p);
answer.fraction =
span<UC const>(before, static_cast<am_digits>(p - before));
if (store_spans) {
answer.fraction =
span<UC const>(before, static_cast<am_digits>(p - before));
}
digit_count -= static_cast<am_digits>(answer.exponent);
#ifndef FASTFLOAT_ONLY_POSITIVE_C_NUMBER_WO_INF_NAN
FASTFLOAT_IF_CONSTEXPR17(basic_json_fmt) {
@ -500,35 +556,41 @@ parse_number_string(UC const *p, UC const *pend,
// We have to check if number has more than 19 significant digits.
if (digit_count > 19) {
answer.too_many_digits = true;
// Let us start again, this time, avoiding overflows.
// We don't need to call if is_integer, since we use the
// pre-tokenized spans from above.
answer.mantissa = 0;
p = answer.integer.ptr;
UC const *int_end = p + answer.integer.len();
constexpr am_mant_t minimal_nineteen_digit_integer{1000000000000000000};
while ((p != int_end) &&
(answer.mantissa < minimal_nineteen_digit_integer)) {
answer.mantissa =
answer.mantissa * 10 + static_cast<am_mant_t>(*p - UC('0'));
++p;
}
if (answer.mantissa >= minimal_nineteen_digit_integer) {
// We have a big integers, so skip the fraction part completely.
answer.exponent = am_pow_t(end_of_integer_part - p) + exp_number;
} else {
// We have a value with a significant fractional component.
p = answer.fraction.ptr;
UC const *const frac_end = p + answer.fraction.len();
while ((p != frac_end) &&
// The truncation recompute below reads the integer/fraction spans. When
// store_spans is false we didn't materialize them, so just flag
// too_many_digits; the caller re-parses with store_spans=true to obtain
// the corrected mantissa/exponent before taking the slow path.
if (store_spans) {
// Let us start again, this time, avoiding overflows.
// We don't need to call if is_integer, since we use the
// pre-tokenized spans from above.
answer.mantissa = 0;
p = answer.integer.ptr;
UC const *int_end = p + answer.integer.len();
constexpr am_mant_t minimal_nineteen_digit_integer{1000000000000000000};
while ((p != int_end) &&
(answer.mantissa < minimal_nineteen_digit_integer)) {
answer.mantissa = static_cast<am_mant_t>(
answer.mantissa * 10 + static_cast<am_mant_t>(*p - UC('0')));
answer.mantissa =
answer.mantissa * 10 + static_cast<am_mant_t>(*p - UC('0'));
++p;
}
answer.exponent = am_pow_t(answer.fraction.ptr - p) + exp_number;
if (answer.mantissa >= minimal_nineteen_digit_integer) {
// We have a big integers, so skip the fraction part completely.
answer.exponent = am_pow_t(end_of_integer_part - p) + exp_number;
} else {
// We have a value with a significant fractional component.
p = answer.fraction.ptr;
UC const *const frac_end = p + answer.fraction.len();
while ((p != frac_end) &&
(answer.mantissa < minimal_nineteen_digit_integer)) {
answer.mantissa = static_cast<am_mant_t>(
answer.mantissa * 10 + static_cast<am_mant_t>(*p - UC('0')));
++p;
}
answer.exponent = am_pow_t(answer.fraction.ptr - p) + exp_number;
}
// We now corrected both exponent and mantissa, to a truncated value
}
// We now corrected both exponent and mantissa, to a truncated value
}
}
@ -576,7 +638,8 @@ parse_int_string(UC const *p, UC const *pend, T &value,
auto const *const start_digits = p;
FASTFLOAT_IF_CONSTEXPR17((std::is_same<T, std::uint8_t>::value)) {
FASTFLOAT_IF_CONSTEXPR17((std::is_same<T, std::uint8_t>::value) &&
sizeof(UC) == 1) {
if (options.base == 10) {
auto const len = static_cast<am_digits>(pend - p);
if (len == 0) {
@ -595,6 +658,7 @@ parse_int_string(UC const *p, UC const *pend, T &value,
if (len >= sizeof(uint32_t)) {
digits = read_chars_to_unsigned<uint32_t>(p);
} else {
uint32_t const b0 = static_cast<uint8_t>(p[0]);
uint32_t const b1 = (len > 1) ? static_cast<uint8_t>(p[1]) : 0x00u;
@ -602,9 +666,6 @@ parse_int_string(UC const *p, UC const *pend, T &value,
uint32_t const b3 = 0x00u;
digits = b0 | (b1 << 8) | (b2 << 16) | (b3 << 24);
}
#if FASTFLOAT_IS_BIG_ENDIAN
digits = byteswap(digits);
#endif
uint32_t const magic =
((digits + 0x46464646u) | (digits - 0x30303030u)) & 0x80808080u;
@ -617,10 +678,9 @@ parse_int_string(UC const *p, UC const *pend, T &value,
answer.ec = std::errc();
answer.ptr = p;
return answer;
} else {
answer.ec = std::errc::invalid_argument;
answer.ptr = first;
}
answer.ec = std::errc::invalid_argument;
answer.ptr = first;
return answer;
}
if (nd > 3) {
@ -655,7 +715,8 @@ parse_int_string(UC const *p, UC const *pend, T &value,
}
}
FASTFLOAT_IF_CONSTEXPR17((std::is_same<T, std::uint16_t>::value)) {
FASTFLOAT_IF_CONSTEXPR17((std::is_same<T, std::uint16_t>::value) &&
sizeof(UC) == 1) {
if (options.base == 10) {
const auto len = static_cast<am_digits>(pend - p);
if (len == 0) {
@ -663,10 +724,10 @@ parse_int_string(UC const *p, UC const *pend, T &value,
value = 0;
answer.ec = std::errc();
answer.ptr = p;
} else {
answer.ec = std::errc::invalid_argument;
answer.ptr = first;
return answer;
}
answer.ec = std::errc::invalid_argument;
answer.ptr = first;
return answer;
}
@ -678,7 +739,7 @@ parse_int_string(UC const *p, UC const *pend, T &value,
v = v * 10 + static_cast<uint32_t>(p[4] - '0');
if (len >= 6 && is_integer(p[5])) {
answer.ec = std::errc::result_out_of_range;
const auto *q = p + 5;
const UC *q = p + 5;
while (q != pend && is_integer(*q)) {
++q;
}
@ -726,10 +787,11 @@ parse_int_string(UC const *p, UC const *pend, T &value,
value = 0;
answer.ec = std::errc();
answer.ptr = p;
} else {
answer.ec = std::errc::invalid_argument;
answer.ptr = first;
return answer;
}
answer.ec = std::errc::invalid_argument;
answer.ptr = first;
return answer;
}
@ -743,9 +805,27 @@ parse_int_string(UC const *p, UC const *pend, T &value,
}
// this check can be eliminated for all other types, but they will all require
// a max_digits(base) equivalent
if (digit_count == max_digits && i < min_safe_u64(options.base)) {
answer.ec = std::errc::result_out_of_range;
return answer;
if (digit_count == max_digits) {
// At the max_digits boundary the accumulator `i` may have wrapped around
// 2^64. A plain `i < min_safe_u64(base)` test is not sufficient: for any
// base whose max_digits-length range exceeds 2^64 (base 10 reaches
// ~5.4 * 2^64 at 20 digits) the value can wrap a whole multiple of 2^64 and
// land back above min_safe, slipping through. Decide exactly in O(1) using
// the leading digit, following the approach used in simdjson:
// ms == min_safe_u64(base) == base^(max_digits-1), the smallest
// max_digits-length value.
// dmax == the largest leading digit whose number can still fit in u64.
// The leading-digit band [d*ms, (d+1)*ms) has width ms < 2^64, so within
// the single band where d == dmax the value straddles 2^64 at most once,
// and a single threshold separates wrapped from non-wrapped values. A
// leading digit above dmax always overflows; below dmax always fits.
uint64_t const ms = min_safe_u64(options.base);
uint64_t const dmax = (std::numeric_limits<uint64_t>::max)() / ms;
uint64_t const lead = ch_to_digit(*start_digits);
if (lead > dmax || (lead == dmax && i < dmax * ms)) {
answer.ec = std::errc::result_out_of_range;
return answer;
}
}
// check other types overflow

View File

@ -7,12 +7,12 @@
namespace fast_float {
/**
* This function parses the character sequence [first,last) for a number. It
* parses floating-point numbers expecting a locale-indepent format equivalent
* to what is used by std::strtod in the default ("C") locale. The resulting
* floating-point value is the closest floating-point values (using either float
* or double), using the "round to even" convention for values that would
* otherwise fall right in-between two values. That is, we provide exact parsing
* according to the IEEE standard.
* parses floating-point numbers expecting a locale-independent format
* equivalent to what is used by std::strtod in the default ("C") locale. The
* resulting floating-point value is the closest floating-point values (using
* either float or double), using the "round to even" convention for values that
* would otherwise fall right in-between two values. That is, we provide exact
* parsing according to the IEEE standard.
*
* Given a successful parse, the pointer (`ptr`) in the returned value is set to
* point right after the parsed number, and the `value` referenced is set to the

View File

@ -18,7 +18,7 @@
#define FASTFLOAT_VERSION_MAJOR 8
#define FASTFLOAT_VERSION_MINOR 2
#define FASTFLOAT_VERSION_PATCH 5
#define FASTFLOAT_VERSION_PATCH 10
#define FASTFLOAT_STRINGIZE_IMPL(x) #x
#define FASTFLOAT_STRINGIZE(x) FASTFLOAT_STRINGIZE_IMPL(x)
@ -227,6 +227,32 @@ using parse_options = parse_options_t<char>;
#define fastfloat_really_inline inline __attribute__((always_inline))
#endif
// Branch-probability hint marking the rare slow-path branches as cold, so the
// optimizer keeps the out-of-line slow-path re-parse off the hot path (and does
// not duplicate the force-inlined hot scanner into the caller, which bloated
// the hot frame and hurt ILP on some targets). Used at the call site as
// if fastfloat_unlikely(cond) { ... }
// (the macro supplies the parentheses). It expands to the standard [[unlikely]]
// attribute when supported, otherwise to __builtin_expect on GCC/Clang, or
// to a no-op elsewhere (e.g. pre-C++20 MSVC, which has no equivalent hint).
#ifdef __has_cpp_attribute
#if __has_cpp_attribute(unlikely) >= 201803L
// g++-9 hits hits this branch, but then fails to compile
// [[unlikely]]. This happens only with g++-9.
#if !defined(__GNUC__) || (__GNUC__ != 9)
#define FASTFLOAT_USE_UNLIKELY_ATTR
#endif
#endif
#endif
#ifdef FASTFLOAT_USE_UNLIKELY_ATTR
#define fastfloat_unlikely(x) (x) [[unlikely]]
#elif defined(__GNUC__) || defined(__clang__)
#define fastfloat_unlikely(x) (__builtin_expect(!!(x), 0))
#else
#define fastfloat_unlikely(x) (x)
#endif
#ifndef FASTFLOAT_ASSERT
#define FASTFLOAT_ASSERT(x) \
{ ((void)(x)); }
@ -1305,7 +1331,11 @@ template <typename T> constexpr uint8_t space_lut<T>::value[];
#endif
template <typename UC> constexpr bool is_space(UC c) {
return c < 256 && space_lut<>::value[uint8_t(c)];
// wchar_t and char can be signed, so a negative code unit slips past a plain
// `c < 256` and then indexes the table by its truncated low byte. Compare as
// unsigned, matching the care taken in ch_to_digit.
using UnsignedUC = typename std::make_unsigned<UC>::type;
return static_cast<UnsignedUC>(c) < 256 && space_lut<>::value[uint8_t(c)];
}
#endif

View File

@ -318,6 +318,30 @@ from_chars_advanced(parsed_number_string_t<UC> const &pns, T &value) noexcept {
return answer;
}
// Slow path: re-parse materializing the integer/fraction spans the hot no-span
// parse skipped, then run the full algorithm. The two callers reach it only
// through a fastfloat_unlikely branch, so the optimizer keeps this re-parse off
// the hot path on its own (no function-level noinline needed).
// from_chars_advanced already handles both the too_many_digits disambiguation
// and the am.power2<0 digit_comp recompute, so both slow branches collapse to
// one helper call.
template <typename T, typename UC>
FASTFLOAT_CONSTEXPR20 from_chars_result_t<UC>
parse_number_slow_path(UC const *first, UC const *last, T &value,
parse_options_t<UC> options
#ifndef FASTFLOAT_ONLY_POSITIVE_C_NUMBER_WO_INF_NAN
,
bool bjf
#endif
) noexcept {
parsed_number_string_t<UC> pns =
#ifndef FASTFLOAT_ONLY_POSITIVE_C_NUMBER_WO_INF_NAN
bjf ? parse_number_string<true, UC>(first, last, options, true) :
#endif
parse_number_string<false, UC>(first, last, options, true);
return from_chars_advanced(pns, value);
}
template <typename T, typename UC>
fastfloat_really_inline FASTFLOAT_CONSTEXPR20 from_chars_result_t<UC>
from_chars_float_advanced(UC const *first, UC const *last, T &value,
@ -347,13 +371,18 @@ from_chars_float_advanced(UC const *first, UC const *last, T &value,
return answer;
}
#endif
parsed_number_string_t<UC> const pns =
#ifndef FASTFLOAT_ONLY_POSITIVE_C_NUMBER_WO_INF_NAN
(chars_format_t(options.format & detail::basic_json_fmt))
? parse_number_string<true, UC>(first, last, options)
:
bool const bjf = chars_format_t(options.format & detail::basic_json_fmt) != 0;
#endif
parse_number_string<false, UC>(first, last, options);
// Fast path: parse WITHOUT materializing the integer/fraction spans (read
// only by the rare slow paths). Skipping their stores keeps the fat
// parsed_number_string_t off the hot path. store_spans is a runtime argument,
// so this reuses the single parse_number_string instantiation.
parsed_number_string_t<UC> pns =
#ifndef FASTFLOAT_ONLY_POSITIVE_C_NUMBER_WO_INF_NAN
bjf ? parse_number_string<true, UC>(first, last, options, false) :
#endif
parse_number_string<false, UC>(first, last, options, false);
if (pns.invalid) {
#ifndef FASTFLOAT_ONLY_POSITIVE_C_NUMBER_WO_INF_NAN
if (chars_format_t(options.format & chars_format::no_infnan)) {
@ -368,8 +397,67 @@ from_chars_float_advanced(UC const *first, UC const *last, T &value,
#endif
}
// call overload that takes parsed_number_string_t directly.
return from_chars_advanced(pns, value);
// Slow path A (rare): > 19 significant digits. The no-span parse left the
// mantissa un-truncated and skipped the span-based recompute; the cold helper
// re-parses with spans and runs the full algorithm.
//
// We have to disable -Wc++20-extensions for the [[unlikely]] attribute
// See comment for @jwakely at
// https://github.com/fastfloat/fast_float/pull/387#discussion_r3366943539
// This is unfortunate.
#ifdef __clang__
#pragma clang diagnostic push
#if (!defined(__APPLE_CC__) && __clang_major__ >= 10) || (__clang_major__ >= 13)
#pragma clang diagnostic ignored "-Wc++20-extensions"
#endif
#endif
if fastfloat_unlikely (pns.too_many_digits) {
return parse_number_slow_path<T, UC>(first, last, value, options
#ifndef FASTFLOAT_ONLY_POSITIVE_C_NUMBER_WO_INF_NAN
,
bjf
#endif
);
}
answer.ec = std::errc(); // be optimistic
answer.ptr = pns.lastmatch;
if (clinger_fast_path_impl(pns.mantissa, pns.exponent,
#ifndef FASTFLOAT_ONLY_POSITIVE_C_NUMBER_WO_INF_NAN
pns.negative,
#endif
value)) {
return answer;
}
adjusted_mantissa am =
compute_float<binary_format<T>>(pns.exponent, pns.mantissa);
// Slow path B (rare): Eisel-Lemire could not resolve; digit_comp needs the
// integer/fraction spans. Route to the cold helper (clinger there is a
// dead-effect since it already failed here; the cold re-parse + digit_comp
// via from_chars_advanced reproduces this branch).
if fastfloat_unlikely (am.power2 < 0) {
return parse_number_slow_path<T, UC>(first, last, value, options
#ifndef FASTFLOAT_ONLY_POSITIVE_C_NUMBER_WO_INF_NAN
,
bjf
#endif
);
}
#ifdef __clang__
#pragma clang diagnostic pop
#endif
to_float(
#ifndef FASTFLOAT_ONLY_POSITIVE_C_NUMBER_WO_INF_NAN
pns.negative,
#endif
am, value);
// Test for over/underflow.
if ((pns.mantissa != 0 && am.mantissa == 0 && am.power2 == 0) ||
am.power2 == binary_format<T>::infinite_power()) {
answer.ec = std::errc::result_out_of_range;
}
return answer;
}
template <typename T, typename UC, typename>

View File

@ -4,6 +4,10 @@ cmake_minimum_required(VERSION 3.11 FATAL_ERROR)
include(FetchContent)
# Some tests (the exhaustive sweeps) parallelize across std::thread.
set(THREADS_PREFER_PTHREAD_FLAG ON)
find_package(Threads REQUIRED)
option(SYSTEM_DOCTEST "Use system copy of doctest" OFF)
option(FASTFLOAT_SUPPLEMENTAL_TESTS "Run supplemental tests" ON)
@ -49,6 +53,7 @@ function(fast_float_add_cpp_test TEST_NAME)
target_compile_options(${TEST_NAME} PUBLIC -Wsign-compare -Wshadow -Wwrite-strings -Wpointer-arith -Winit-self -Wconversion -Wsign-conversion)
endif()
target_link_libraries(${TEST_NAME} PUBLIC fast_float supplemental-data)
target_link_libraries(${TEST_NAME} PUBLIC Threads::Threads)
if (NOT SYSTEM_DOCTEST)
target_link_libraries(${TEST_NAME} PUBLIC doctest)
else ()

View File

@ -8,6 +8,8 @@
#include <iostream>
#include <limits>
#include <system_error>
#include <thread>
#include <vector>
template <typename T> char *to_string(T d, char *buffer) {
auto written = std::snprintf(buffer, 64, "%.*e",
@ -15,47 +17,59 @@ template <typename T> char *to_string(T d, char *buffer) {
return buffer + written;
}
void allvalues() {
// Checks a single 32-bit word (interpreted as a float); aborts on a mismatch.
void check_word(uint32_t word) {
char buffer[64];
for (uint64_t w = 0; w <= 0xFFFFFFFF; w++) {
float v;
if ((w % 1048576) == 0) {
std::cout << ".";
std::cout.flush();
}
uint32_t word = uint32_t(w);
memcpy(&v, &word, sizeof(v));
float v;
memcpy(&v, &word, sizeof(v));
{
char const *string_end = to_string(v, buffer);
float result_value;
auto result = fast_float::from_chars(buffer, string_end, result_value);
// Starting with version 4.0 for fast_float, we return result_out_of_range
// if the value is either too small (too close to zero) or too large
// (effectively infinity). So std::errc::result_out_of_range is normal for
// well-formed input strings.
if (result.ec != std::errc() &&
result.ec != std::errc::result_out_of_range) {
std::cerr << "parsing error ? " << buffer << std::endl;
abort();
}
if (std::isnan(v)) {
if (!std::isnan(result_value)) {
std::cerr << "not nan" << buffer << std::endl;
abort();
}
} else if (copysign(1, result_value) != copysign(1, v)) {
std::cerr << "I got " << std::hexfloat << result_value
<< " but I was expecting " << v << std::endl;
abort();
} else if (result_value != v) {
std::cerr << "no match ? " << buffer << std::endl;
std::cout << "started with " << std::hexfloat << v << std::endl;
std::cout << "got back " << std::hexfloat << result_value << std::endl;
std::cout << std::dec;
abort();
}
char const *string_end = to_string(v, buffer);
float result_value;
auto result = fast_float::from_chars(buffer, string_end, result_value);
// Starting with version 4.0 for fast_float, we return result_out_of_range
// if the value is either too small (too close to zero) or too large
// (effectively infinity). So std::errc::result_out_of_range is normal for
// well-formed input strings.
if (result.ec != std::errc() && result.ec != std::errc::result_out_of_range) {
std::cerr << "parsing error ? " << buffer << std::endl;
abort();
}
if (std::isnan(v)) {
if (!std::isnan(result_value)) {
std::cerr << "not nan" << buffer << std::endl;
abort();
}
} else if (copysign(1, result_value) != copysign(1, v)) {
std::cerr << "I got " << std::hexfloat << result_value
<< " but I was expecting " << v << std::endl;
abort();
} else if (result_value != v) {
std::cerr << "no match ? " << buffer << std::endl;
std::cout << "started with " << std::hexfloat << v << std::endl;
std::cout << "got back " << std::hexfloat << result_value << std::endl;
std::cout << std::dec;
abort();
}
}
// Sweeps the whole 2^32 float space, split across hardware threads (the values
// are independent); check_word() aborts on the first mismatch.
void allvalues() {
unsigned int nthreads = std::thread::hardware_concurrency();
if (nthreads == 0) {
nthreads = 1;
}
std::vector<std::thread> workers;
workers.reserve(nthreads);
for (unsigned int t = 0; t < nthreads; t++) {
workers.emplace_back([t, nthreads]() {
for (uint64_t w = t; w <= 0xFFFFFFFF; w += nthreads) {
check_word(uint32_t(w));
}
});
}
for (std::thread &worker : workers) {
worker.join();
}
std::cout << std::endl;
}

View File

@ -1,6 +1,7 @@
#include "fast_float/fast_float.h"
#include <atomic>
#include <cassert>
#include <cmath>
#include <cstdio>
@ -9,6 +10,8 @@
#include <limits>
#include <string>
#include <system_error>
#include <thread>
#include <vector>
template <typename T> char *to_string(T d, char *buffer) {
auto written = std::snprintf(buffer, 64, "%.*e",
@ -45,25 +48,38 @@ bool basic_test_64bit(std::string vals, double val) {
return true;
}
// Sweeps the whole 2^32 float space (widened to double), split across hardware
// threads (the values are independent); stops at the first mismatch.
void all_32bit_values() {
char buffer[64];
for (uint64_t w = 0; w <= 0xFFFFFFFF; w++) {
float v32;
if ((w % 1048576) == 0) {
std::cout << ".";
std::cout.flush();
}
uint32_t word = uint32_t(w);
memcpy(&v32, &word, sizeof(v32));
double v = v32;
unsigned int nthreads = std::thread::hardware_concurrency();
if (nthreads == 0) {
nthreads = 1;
}
std::atomic<bool> ok{true};
std::vector<std::thread> workers;
workers.reserve(nthreads);
for (unsigned int t = 0; t < nthreads; t++) {
workers.emplace_back([t, nthreads, &ok]() {
char buffer[64];
for (uint64_t w = t;
w <= 0xFFFFFFFF && ok.load(std::memory_order_relaxed);
w += nthreads) {
float v32;
uint32_t word = uint32_t(w);
memcpy(&v32, &word, sizeof(v32));
double v = v32;
{
char const *string_end = to_string(v, buffer);
std::string s(buffer, size_t(string_end - buffer));
if (!basic_test_64bit(s, v)) {
return;
char const *string_end = to_string(v, buffer);
std::string s(buffer, size_t(string_end - buffer));
if (!basic_test_64bit(s, v)) {
ok.store(false, std::memory_order_relaxed);
return;
}
}
}
});
}
for (std::thread &worker : workers) {
worker.join();
}
std::cout << std::endl;
}

View File

@ -1,5 +1,6 @@
#include "fast_float/fast_float.h"
#include <atomic>
#include <cassert>
#include <cmath>
#include <cstdio>
@ -7,6 +8,8 @@
#include <iostream>
#include <limits>
#include <stdexcept>
#include <thread>
#include <vector>
#if defined(__CYGWIN__) || defined(__MINGW32__) || defined(__MINGW64__)
// Anything at all that is related to cygwin, msys and so forth will
@ -74,86 +77,107 @@ void strtof_from_string(char const *st, float &d) {
}
}
bool allvalues() {
// Checks a single 32-bit word (interpreted as a float). Returns true if the
// parser agrees with the reference, false (after logging) on a mismatch.
bool check_word(uint32_t word) {
char buffer[64];
for (uint64_t w = 0; w <= 0xFFFFFFFF; w++) {
float v;
if ((w % 1048576) == 0) {
std::cout << ".";
std::cout.flush();
}
uint32_t word = uint32_t(w);
memcpy(&v, &word, sizeof(v));
if (std::isfinite(v)) {
float nextf = std::nextafterf(v, INFINITY);
if (copysign(1, v) != copysign(1, nextf)) {
continue;
}
if (!std::isfinite(nextf)) {
continue;
}
double v1{v};
assert(float(v1) == v);
double v2{nextf};
assert(float(v2) == nextf);
double midv{v1 + (v2 - v1) / 2};
float expected_midv = float(midv);
char const *string_end = to_string(midv, buffer);
float str_answer;
strtof_from_string(buffer, str_answer);
float result_value;
auto result = fast_float::from_chars(buffer, string_end, result_value);
// Starting with version 4.0 for fast_float, we return result_out_of_range
// if the value is either too small (too close to zero) or too large
// (effectively infinity). So std::errc::result_out_of_range is normal for
// well-formed input strings.
if (result.ec != std::errc() &&
result.ec != std::errc::result_out_of_range) {
std::cerr << "parsing error ? " << buffer << std::endl;
return false;
}
if (std::isnan(v)) {
if (!std::isnan(result_value)) {
std::cerr << "not nan" << buffer << std::endl;
std::cerr << "v " << std::hexfloat << v << std::endl;
std::cerr << "v2 " << std::hexfloat << v2 << std::endl;
std::cerr << "midv " << std::hexfloat << midv << std::endl;
std::cerr << "expected_midv " << std::hexfloat << expected_midv
<< std::endl;
return false;
}
} else if (copysign(1, result_value) != copysign(1, v)) {
std::cerr << buffer << std::endl;
std::cerr << "v " << std::hexfloat << v << std::endl;
std::cerr << "v2 " << std::hexfloat << v2 << std::endl;
std::cerr << "midv " << std::hexfloat << midv << std::endl;
std::cerr << "expected_midv " << std::hexfloat << expected_midv
<< std::endl;
std::cerr << "I got " << std::hexfloat << result_value
<< " but I was expecting " << v << std::endl;
return false;
} else if (result_value != str_answer) {
std::cerr << "no match ? " << buffer << std::endl;
std::cerr << "v " << std::hexfloat << v << std::endl;
std::cerr << "v2 " << std::hexfloat << v2 << std::endl;
std::cerr << "midv " << std::hexfloat << midv << std::endl;
std::cerr << "expected_midv " << std::hexfloat << expected_midv
<< std::endl;
std::cout << "started with " << std::hexfloat << midv << std::endl;
std::cout << "round down to " << std::hexfloat << str_answer
<< std::endl;
std::cout << "got back " << std::hexfloat << result_value << std::endl;
std::cout << std::dec;
return false;
}
}
float v;
memcpy(&v, &word, sizeof(v));
if (!std::isfinite(v)) {
return true;
}
float nextf = std::nextafterf(v, INFINITY);
if (copysign(1, v) != copysign(1, nextf)) {
return true;
}
if (!std::isfinite(nextf)) {
return true;
}
double v1{v};
assert(float(v1) == v);
double v2{nextf};
assert(float(v2) == nextf);
double midv{v1 + (v2 - v1) / 2};
float expected_midv = float(midv);
char const *string_end = to_string(midv, buffer);
float str_answer;
strtof_from_string(buffer, str_answer);
float result_value;
auto result = fast_float::from_chars(buffer, string_end, result_value);
// Starting with version 4.0 for fast_float, we return result_out_of_range
// if the value is either too small (too close to zero) or too large
// (effectively infinity). So std::errc::result_out_of_range is normal for
// well-formed input strings.
if (result.ec != std::errc() && result.ec != std::errc::result_out_of_range) {
std::cerr << "parsing error ? " << buffer << std::endl;
return false;
}
if (std::isnan(v)) {
if (!std::isnan(result_value)) {
std::cerr << "not nan" << buffer << std::endl;
std::cerr << "v " << std::hexfloat << v << std::endl;
std::cerr << "v2 " << std::hexfloat << v2 << std::endl;
std::cerr << "midv " << std::hexfloat << midv << std::endl;
std::cerr << "expected_midv " << std::hexfloat << expected_midv
<< std::endl;
return false;
}
} else if (copysign(1, result_value) != copysign(1, v)) {
std::cerr << buffer << std::endl;
std::cerr << "v " << std::hexfloat << v << std::endl;
std::cerr << "v2 " << std::hexfloat << v2 << std::endl;
std::cerr << "midv " << std::hexfloat << midv << std::endl;
std::cerr << "expected_midv " << std::hexfloat << expected_midv
<< std::endl;
std::cerr << "I got " << std::hexfloat << result_value
<< " but I was expecting " << v << std::endl;
return false;
} else if (result_value != str_answer) {
std::cerr << "no match ? " << buffer << std::endl;
std::cerr << "v " << std::hexfloat << v << std::endl;
std::cerr << "v2 " << std::hexfloat << v2 << std::endl;
std::cerr << "midv " << std::hexfloat << midv << std::endl;
std::cerr << "expected_midv " << std::hexfloat << expected_midv
<< std::endl;
std::cout << "started with " << std::hexfloat << midv << std::endl;
std::cout << "round down to " << std::hexfloat << str_answer << std::endl;
std::cout << "got back " << std::hexfloat << result_value << std::endl;
std::cout << std::dec;
return false;
}
std::cout << std::endl;
return true;
}
// Sweeps the whole 2^32 float space, split across hardware threads (the values
// are independent). Returns false as soon as any word mismatches.
bool allvalues() {
unsigned int nthreads = std::thread::hardware_concurrency();
if (nthreads == 0) {
nthreads = 1;
}
std::atomic<bool> ok{true};
std::vector<std::thread> workers;
workers.reserve(nthreads);
for (unsigned int t = 0; t < nthreads; t++) {
workers.emplace_back([t, nthreads, &ok]() {
for (uint64_t w = t;
w <= 0xFFFFFFFF && ok.load(std::memory_order_relaxed);
w += nthreads) {
if (!check_word(uint32_t(w))) {
ok.store(false, std::memory_order_relaxed);
return;
}
}
});
}
for (std::thread &worker : workers) {
worker.join();
}
return ok.load();
}
inline void Assert(bool Assertion) {
#if defined(__CYGWIN__) || defined(__MINGW32__) || defined(__MINGW64__) || \
defined(sun) || defined(__sun)

View File

@ -17,7 +17,10 @@
#include <iostream>
#include <vector>
#include <string_view>
#include <string>
#include <cstring>
#include <random>
#include <algorithm>
#include "fast_float/fast_float.h"
#include <cstdint>
@ -821,6 +824,61 @@ int main() {
++base_unsigned;
}
// unsigned out of range error base test, multi-wrap (64 bit)
// These values overflow uint64_t, but the accumulator wraps a whole multiple
// of 2^64 and lands back at or above the smallest max_digits-length value, so
// a single comparison against that bound does not catch the overflow. Bases
// 2, 4 and 16 are excluded because their max_digits-length range fits within
// a single 2^64 span.
std::vector<int> const unsigned_multiwrap_base{
3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 17, 18, 19, 20,
21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36};
std::vector<std::string_view> const unsigned_multiwrap_base_test{
"22222222222222222222222222222222222222222",
"4400000000000000000000000000",
"5555555555555555555555555",
"66666666666666666666666",
"7777777777777777777777",
"888888888888888888888",
"46893488147419103233",
"AAAAAAAAAAAAAAAAAAA",
"BBBBBBBBBBBBBBBBBB",
"427772311192C9BAAB",
"DDDDDDDDDDDDDDDDD",
"532C82996D3A44919",
"GGGGGGGGGGGGGGGG",
"HHHHHHHHHHHHHHHH",
"3835GEGDF36622EG",
"JJJJJJJJJJJJJJJ",
"KKKKKKKKKKKKKKK",
"LLLLLLLLLLLLLLL",
"444BGHB4EG5DA2D",
"NNNNNNNNNNNNNN",
"JE5H4MNDLJGNLO",
"PPPPPPPPPPPPPP",
"QQQQQQQQQQQQQQ",
"RRRRRRRRRRRRRR",
"4H7QS52310IHQK",
"TTTTTTTTTTTTTT",
"UUUUUUUUUUUUU",
"VVVVVVVVVVVVV",
"WWWWWWWWWWWWW",
"XXXXXXXXXXXXX",
"YYYYYYYYYYYYY",
"6U831JL976P6O"};
for (std::size_t i = 0; i < unsigned_multiwrap_base_test.size(); ++i) {
auto const &f = unsigned_multiwrap_base_test[i];
uint64_t result;
auto answer = fast_float::from_chars(f.data(), f.data() + f.size(), result,
unsigned_multiwrap_base[i]);
if (answer.ec != std::errc::result_out_of_range) {
std::cerr << "expected error for should be 'result_out_of_range': \"" << f
<< "\"" << std::endl;
return EXIT_FAILURE;
}
}
// just within range base test (64 bit)
std::vector<std::string_view> const int_within_range_base_test{
"111111111111111111111111111111111111111111111111111111111111111",
@ -1295,6 +1353,330 @@ int main() {
return EXIT_FAILURE;
}
}
// The uint8_t and uint16_t base-10 paths use a byte-oriented fast path. A
// wider code unit whose low byte is an ASCII digit (e.g. U+2131..U+2139) must
// not be mistaken for that digit. The generic path already rejects these for
// int; the fixed-width fast paths must agree. Lengths of 1..5 exercise both
// the uint8_t path and the 4-digit uint16_t SWAR path.
{
const std::u16string bad16[] = {
u"", u"ℱℲ", u"ℱℲℳ", u"ℱℲℳℴ", u"ℱℲℳℴℵ",
};
const std::u32string bad32[] = {
U"",
U"ℱℲℳ",
U"ℱℲℳℴ",
U"ℱℲℳℴℵ",
};
bool failed = false;
for (auto const &s : bad16) {
uint8_t r8 = 123;
auto a8 = fast_float::from_chars(s.data(), s.data() + s.size(), r8);
if (a8.ec == std::errc()) {
failed = true;
std::cerr << "Incorrectly parsed wide units as uint8_t " << unsigned(r8)
<< "." << std::endl;
}
uint16_t r16 = 123;
auto a16 = fast_float::from_chars(s.data(), s.data() + s.size(), r16);
if (a16.ec == std::errc()) {
failed = true;
std::cerr << "Incorrectly parsed wide units as uint16_t " << r16 << "."
<< std::endl;
}
}
for (auto const &s : bad32) {
uint8_t r8 = 123;
auto a8 = fast_float::from_chars(s.data(), s.data() + s.size(), r8);
if (a8.ec == std::errc()) {
failed = true;
std::cerr << "Incorrectly parsed wide units as uint8_t " << unsigned(r8)
<< "." << std::endl;
}
uint16_t r16 = 123;
auto a16 = fast_float::from_chars(s.data(), s.data() + s.size(), r16);
if (a16.ec == std::errc()) {
failed = true;
std::cerr << "Incorrectly parsed wide units as uint16_t " << r16 << "."
<< std::endl;
}
}
if (failed) {
return EXIT_FAILURE;
}
}
// Comprehensive, oracle-checked u64 overflow detection across every base.
//
// The accumulator in parse_int_string is allowed to overflow and the result
// is validated afterwards. At the max_digits boundary a value can wrap one or
// more whole multiples of 2^64 (a 20-digit base-10 number reaches ~5.4*2^64),
// so the boundary check must be exact. This section validates from_chars for
// bases 2..36 against an independent, trusted oracle: a plain 64-bit checked
// multiply-add. It hammers the single leading-digit band that straddles 2^64
// (where wrapped and non-wrapped values are hardest to tell apart) and also
// covers max_digits-1 (always in range) and max_digits+1 (always overflow).
{
auto digit_to_char = [](int d) -> char {
return d < 10 ? char('0' + d) : char('A' + (d - 10));
};
auto char_to_digit = [](char c) -> int {
if (c >= '0' && c <= '9') {
return c - '0';
}
if (c >= 'A' && c <= 'Z') {
return c - 'A' + 10;
}
return c - 'a' + 10;
};
// Trusted oracle: parse `s` in `base` with a checked 64-bit multiply-add.
// Returns true on u64 overflow; otherwise writes the value to `out`.
auto oracle = [&](std::string const &s, int base, uint64_t &out) -> bool {
uint64_t v = 0;
for (char c : s) {
uint64_t const d = uint64_t(char_to_digit(c));
if (v > (UINT64_MAX - d) / uint64_t(base)) {
return true;
}
v = uint64_t(base) * v + d;
}
out = v;
return false;
};
auto to_base = [&](uint64_t v, int base) -> std::string {
if (v == 0) {
return "0";
}
std::string s;
while (v != 0) {
s += digit_to_char(int(v % uint64_t(base)));
v /= uint64_t(base);
}
std::reverse(s.begin(), s.end());
return s;
};
// Add one (in base `base`) to the digit string `s`, carrying as needed.
auto increment = [&](std::string s, int base) -> std::string {
int carry = 1;
for (std::size_t k = s.size(); k-- > 0 && carry != 0;) {
int const d = char_to_digit(s[k]) + carry;
carry = d / base;
s[k] = digit_to_char(d % base);
}
if (carry != 0) {
s.insert(s.begin(), digit_to_char(carry));
}
return s;
};
// Subtract one (in base `base`) from a non-zero, non-negative string.
auto decrement = [&](std::string s, int base) -> std::string {
int borrow = 1;
for (std::size_t k = s.size(); k-- > 0 && borrow != 0;) {
int d = char_to_digit(s[k]) - borrow;
borrow = d < 0 ? 1 : 0;
if (d < 0) {
d += base;
}
s[k] = digit_to_char(d);
}
std::size_t lead = s.find_first_not_of('0'); // drop any leading zero
return lead == std::string::npos ? "0" : s.substr(lead);
};
std::mt19937_64 rng(0xC0FFEEULL);
long long checked = 0;
auto verify = [&](std::string const &s, int base) -> bool {
uint64_t expected = 0;
bool const ov = oracle(s, base, expected);
uint64_t result = 0xDEADBEEFULL;
auto answer =
fast_float::from_chars(s.data(), s.data() + s.size(), result, base);
++checked;
if (ov) {
if (answer.ec != std::errc::result_out_of_range) {
std::cerr << "base " << base
<< ": expected result_out_of_range for \"" << s << "\""
<< std::endl;
return false;
}
} else {
if (answer.ec != std::errc()) {
std::cerr << "base " << base << ": unexpected error for \"" << s
<< "\"" << std::endl;
return false;
}
if (result != expected) {
std::cerr << "base " << base << ": \"" << s << "\" -> " << result
<< ", expected " << expected << std::endl;
return false;
}
if (answer.ptr != s.data() + s.size()) {
std::cerr << "base " << base << ": did not consume all of \"" << s
<< "\"" << std::endl;
return false;
}
}
return true;
};
// Leading zeros are stripped before the digit count, so the outcome must be
// unchanged. Checked only on hand-picked values (it exercises shared code).
auto verify_zeros = [&](std::string const &digits, int base) -> bool {
return verify(digits, base) && verify("0" + digits, base) &&
verify(std::string(40, '0') + digits, base);
};
auto random_tail = [&](std::string &s, int n, int base) {
for (int k = 0; k < n; ++k) {
// bias toward the extremes (0 and base-1) to hit boundaries often
std::uint64_t const r = rng();
int const mode = int(r % 4);
int const dig = mode == 0 ? 0
: mode == 1 ? base - 1
: int((r >> 2) % std::uint64_t(base));
s += digit_to_char(dig);
}
};
for (int base = 2; base <= 36; ++base) {
// M = max number of base-`base` digits a u64 can hold.
std::string const maxstr = to_base(UINT64_MAX, base);
int const M = int(maxstr.size());
// b^(M-1): smallest M-digit value, and width of each leading-digit band.
uint64_t bM1 = 1;
for (int k = 0; k < M - 1; ++k) {
bM1 *= uint64_t(base);
}
int const dmax = int(UINT64_MAX / bM1); // largest leading digit that fits
// Exact-boundary sweep straddling 2^64 (the hardest transition): the
// 64 values UINT64_MAX-31 .. UINT64_MAX (in range) and 2^64 .. 2^64+31
// (overflow), built by walking the digit string up and down.
std::string below = maxstr, above = increment(maxstr, base);
for (int k = 0; k < 32; ++k) {
if (!verify(below, base) || !verify(above, base)) {
return EXIT_FAILURE;
}
below = decrement(below, base);
above = increment(above, base);
}
// Hand-picked values, also checked with leading zeros.
std::string const allmax(std::size_t(M), digit_to_char(base - 1));
if (!verify_zeros(maxstr, base) || // largest in-range value
!verify_zeros(increment(maxstr, base), base) || // smallest overflow
!verify_zeros(allmax, base)) { // largest M-digit (multi-wrap)
return EXIT_FAILURE;
}
// Randomized M-digit values across every leading digit. Bands with
// lead > dmax always overflow (this is where the naive min_safe check
// wrongly accepted multi-wrap values); lead < dmax always fits; lead ==
// dmax straddles 2^64 and gets the heaviest sampling.
for (int lead = 1; lead < base; ++lead) {
int const trials = lead == dmax ? 4000 : 300;
for (int trial = 0; trial < trials; ++trial) {
std::string s(1, digit_to_char(lead));
random_tail(s, M - 1, base);
if (!verify(s, base)) {
return EXIT_FAILURE;
}
}
}
// max_digits-1 digits never overflow; max_digits+1 digits always do.
for (int trial = 0; trial < 500; ++trial) {
std::string shorts(1,
digit_to_char(1 + int(rng() % uint64_t(base - 1))));
random_tail(shorts, M - 2, base);
std::string longs(1,
digit_to_char(1 + int(rng() % uint64_t(base - 1))));
random_tail(longs, M, base);
if (!verify(shorts, base) || !verify(longs, base)) {
return EXIT_FAILURE;
}
}
}
if (checked < 100000) {
std::cerr << "overflow sweep ran too few cases: " << checked << std::endl;
return EXIT_FAILURE;
}
}
// Signed (int64_t) boundary: every value that overflows u64 also overflows
// i64, and the exact i64 limits must parse. Reuses the oracle indirectly via
// hand-built extremes per base.
{
auto digit_to_char = [](int d) -> char {
return d < 10 ? char('0' + d) : char('A' + (d - 10));
};
auto to_base_signed = [&](int64_t value, int base) -> std::string {
// value may be INT64_MIN; accumulate magnitude in u64 to avoid UB.
bool const neg = value < 0;
uint64_t mag = neg ? (~uint64_t(value) + 1) : uint64_t(value);
std::string s;
if (mag == 0) {
s += '0';
}
while (mag != 0) {
s += digit_to_char(int(mag % uint64_t(base)));
mag /= uint64_t(base);
}
if (neg) {
s += '-';
}
std::reverse(s.begin(), s.end());
return s;
};
for (int base = 2; base <= 36; ++base) {
struct {
int64_t v;
} const limits[] = {{INT64_MAX}, {INT64_MIN}, {0}, {-1}, {1}};
for (auto const &lim : limits) {
std::string const s = to_base_signed(lim.v, base);
int64_t result = 123;
auto answer =
fast_float::from_chars(s.data(), s.data() + s.size(), result, base);
if (answer.ec != std::errc() || result != lim.v) {
std::cerr << "base " << base << ": signed limit \"" << s
<< "\" failed to round-trip (got " << result << ")"
<< std::endl;
return EXIT_FAILURE;
}
}
// Increment a non-negative magnitude string (in `base`) by one.
auto inc_mag = [&](std::string m) -> std::string {
int carry = 1;
for (std::size_t k = m.size(); k-- > 0 && carry != 0;) {
int d = (m[k] >= '0' && m[k] <= '9') ? m[k] - '0'
: (m[k] >= 'A' && m[k] <= 'Z') ? m[k] - 'A' + 10
: m[k] - 'a' + 10;
d += carry;
carry = d / base;
m[k] = digit_to_char(d % base);
}
if (carry != 0) {
m.insert(m.begin(), digit_to_char(carry));
}
return m;
};
// INT64_MAX + 1 (= 2^63) overflows a positive int64_t.
// INT64_MIN - 1 (= -(2^63 + 1)) overflows a negative int64_t.
// Note that -(2^63) == INT64_MIN is in range and is covered above.
std::string const max_mag = to_base_signed(INT64_MAX, base); // 2^63 - 1
std::string const over = inc_mag(max_mag); // 2^63
std::string const under = "-" + inc_mag(over); // -(2^63 + 1)
for (std::string const &s : {over, under}) {
int64_t result = 123;
auto answer =
fast_float::from_chars(s.data(), s.data() + s.size(), result, base);
if (answer.ec != std::errc::result_out_of_range) {
std::cerr << "base " << base << ": expected result_out_of_range for "
<< "signed \"" << s << "\"" << std::endl;
return EXIT_FAILURE;
}
}
}
}
return EXIT_SUCCESS;
}

View File

@ -65,6 +65,4 @@ int main() {
#endif
std::cout << "All tests passed successfully." << std::endl;
return EXIT_SUCCESS;
return 0;
}
}

View File

@ -45,11 +45,6 @@ void all_32bit_values() {
std::cerr << "I got " << std::hexfloat << result_value
<< " but I was expecting " << v << std::endl;
abort();
} else if (std::isnan(v)) {
if (!std::isnan(result_value)) {
std::cerr << "not nan" << buffer << std::endl;
abort();
}
} else if (result_value != v) {
std::cerr << "no match ? " << buffer << std::endl;
std::cout << "started with " << std::hexfloat << v << std::endl;

View File

@ -2,6 +2,7 @@
#include <iostream>
#include <string>
#include <system_error>
#include <type_traits>
bool tester(std::string s, double expected,
fast_float::chars_format fmt = fast_float::chars_format::general) {
@ -46,8 +47,32 @@ bool test_nan() {
return tester("nan", std::numeric_limits<double>::quiet_NaN());
}
// A wide code unit whose low byte is an ASCII space (0x20) but whose full value
// is not whitespace must not be skipped by skip_white_space. When wchar_t is
// signed such a unit can be negative, which used to slip past the range guard
// in is_space and get treated as a space.
bool test_non_space_with_space_low_byte() {
if (!std::is_signed<wchar_t>::value) {
return true; // only reproducible where wchar_t is signed
}
std::wstring input = L" 42";
// 0x...FF20: low byte 0x20, high bits set, so the value is negative.
input[0] = static_cast<wchar_t>(~static_cast<unsigned int>(0xFF) | 0x20u);
double result;
auto answer =
fast_float::from_chars(input.data(), input.data() + input.size(), result,
fast_float::chars_format::general |
fast_float::chars_format::skip_white_space);
if (answer.ec == std::errc()) {
std::cerr << "leading non-space code unit must not be skipped\n";
return false;
}
return true;
}
int main() {
if (test_minus() && test_plus() && test_space() && test_nan()) {
if (test_minus() && test_plus() && test_space() && test_nan() &&
test_non_space_with_space_low_byte()) {
std::cout << "all ok" << std::endl;
return EXIT_SUCCESS;
}