Skip to content
6 changes: 6 additions & 0 deletions .github/workflows/c.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,9 @@ jobs:
run: sudo apt-get update && sudo apt-get install -y gcc-multilib
- name: make test
run: make test
- name: cmake configure
run: cmake -B build
- name: cmake build
run: cmake --build build -j 2
- name: cmake test
run: ctest --test-dir build -j 2
1 change: 0 additions & 1 deletion example.c
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
#include <stdio.h>
#include <string.h>

#define FFC_IMPL
#include "ffc.h"

int main(void) {
Expand Down
89 changes: 88 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ Example
#include <stdio.h>
#include <string.h>

#define FFC_IMPL
#include "ffc.h"

int main(void) {
Expand All @@ -30,8 +29,96 @@ int main(void) {
For use within a larger parser, where you don't expect to reach the end of input, use
the non-simple variants as the `ffc_result` includes the stopping point, just like in fast_float

## API

### Float Parsing

- `double ffc_parse_double_simple(size_t len, const char *s, ffc_outcome *outcome)`
Parses a double from a string of given length. Returns the parsed value, outcome indicates success/failure.

- `ffc_result ffc_parse_double(size_t len, const char *s, double *out)`
Parses a double from a string, storing result in `out`. Returns `ffc_result` with outcome and end pointer.

- `float ffc_parse_float_simple(size_t len, const char *s, ffc_outcome *outcome)`
Parses a float from a string of given length. Returns the parsed value.

- `ffc_result ffc_parse_float(size_t len, const char *s, float *out)`
Parses a float from a string, storing result in `out`.

### Integer Parsing

- `ffc_result ffc_parse_i64(size_t len, const char *s, int base, int64_t *out)`
Parses a signed 64-bit integer from string with given base.

- `ffc_result ffc_parse_u64(size_t len, const char *s, int base, uint64_t *out)`
Parses an unsigned 64-bit integer from string with given base.

### Types

- `ffc_outcome`: Enum indicating parse result (OK, OUT_OF_RANGE, INVALID_INPUT)
- `ffc_result`: Struct with `ptr` (end of parsed string) and `outcome`

## Building

### With Make

Use the provided Makefile:

```bash
make test
make example
```

### With CMake

ffc.h supports building with CMake as an installable single-header library.

```bash
cmake -B build
cmake --build build
ctest --test-dir build
cmake --install build # Install the library
```

The CMake build creates test executables for both the amalgamated header and the separate src/ headers.


To use ffc.h as a dependency in your CMake project, you have two options:

#### Using FetchContent

```cmake
include(FetchContent)
FetchContent_Declare(
ffc
GIT_REPOSITORY https://github.com/dlemire/ffc.h.git
GIT_TAG main
)
FetchContent_MakeAvailable(ffc)
target_link_libraries(your_target ffc::ffc)
```

#### Using CPM.cmake

```cmake
include(cpm)
CPMAddPackage("gh:dlemire/ffc.h#main")
target_link_libraries(your_target ffc::ffc)
```


## Caveats
- I have not benchmarked yet; we need to confirm that constant folding, and thus branch elimination, is occurring
as intended for float/double paths and the 4 integer paths.
- Does not support wide chars; only 1-byte strings (e.g., UTF8) are supported.
- The 32-bit architecture code is untested


## References

* Daniel Lemire, [Number Parsing at a Gigabyte per
Second](https://arxiv.org/abs/2101.11408), Software: Practice and Experience
51 (8), 2021.
* Noble Mushtak, Daniel Lemire, [Fast Number Parsing Without
Fallback](https://arxiv.org/abs/2212.06644), Software: Practice and Experience
53 (7), 2023.
138 changes: 138 additions & 0 deletions test_src/supplemental_tests.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
#include "ffc.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fenv.h>
#include <stdint.h>
#include <dirent.h>
#include <sys/stat.h>
#include <errno.h>
#include <time.h>

const char* round_name(int d) {
switch (d) {
case FE_UPWARD: return "FE_UPWARD";
case FE_DOWNWARD: return "FE_DOWNWARD";
case FE_TOWARDZERO: return "FE_TOWARDZERO";
case FE_TONEAREST: return "FE_TONEAREST";
default: return "UNKNOWN";
}
}

// return 1 on success, 0 on failure
int check_file(const char* file_name) {
printf("Checking %s\n", file_name);
// We check all rounding directions, for each file.
int directions[] = {FE_UPWARD, FE_DOWNWARD, FE_TOWARDZERO, FE_TONEAREST};
size_t num_directions = sizeof(directions) / sizeof(directions[0]);
for (size_t i = 0; i < num_directions; i++) {
int d = directions[i];
fesetround(d);
size_t number = 0;
FILE* file = fopen(file_name, "r");
if (file) {
char str[4096];
while (fgets(str, sizeof(str), file)) {
size_t len = strlen(str);
if (len > 0 && str[len-1] == '\n') str[len-1] = '\0';
if (strlen(str) > 0) {
// Skip float16 for now, as C support may vary
// Read 32-bit hex
uint32_t float32 = 0;
if (sscanf(str + 5, "%x", &float32) != 1) {
fprintf(stderr, "32-bit parsing failure: %s\n", str);
fclose(file);
fesetround(FE_TONEAREST);
return 0;
}
// Read 64-bit hex
uint64_t float64 = 0;
if (sscanf(str + 14, "%llx", &float64) != 1) {
fprintf(stderr, "64-bit parsing failure: %s\n", str);
fclose(file);
fesetround(FE_TONEAREST);
return 0;
}
// The string to parse:
const char* number_string = str + 31;
size_t str_len = strlen(number_string);
// Parse as 32-bit float
float parsed_32;
ffc_result result32 = ffc_parse_float(str_len, number_string, &parsed_32);
if (result32.outcome != FFC_OUTCOME_OK && result32.outcome != FFC_OUTCOME_OUT_OF_RANGE) {
fprintf(stderr, "32-bit ffc parsing failure: %s\n", str);
fclose(file);
fesetround(FE_TONEAREST);
return 0;
}
// Parse as 64-bit float
double parsed_64;
ffc_result result64 = ffc_parse_double(str_len, number_string, &parsed_64);
if (result64.outcome != FFC_OUTCOME_OK && result64.outcome != FFC_OUTCOME_OUT_OF_RANGE) {
fprintf(stderr, "64-bit ffc parsing failure: %s\n", str);
fclose(file);
fesetround(FE_TONEAREST);
return 0;
}
// Convert the floats to unsigned ints.
uint32_t float32_parsed = 0;
uint64_t float64_parsed = 0;
memcpy(&float32_parsed, &parsed_32, sizeof(parsed_32));
memcpy(&float64_parsed, &parsed_64, sizeof(parsed_64));
// Compare with expected results
if (float32_parsed != float32) {
printf("bad 32: %s\n", str);
printf("parsed as %f\n", parsed_32);
printf("as raw uint32_t, parsed = %u, expected = %u\n", float32_parsed, float32);
printf("fesetround: %s\n", round_name(d));
fclose(file);
fesetround(FE_TONEAREST);
return 0;
}
if (float64_parsed != float64) {
printf("bad 64: %s\n", str);
printf("parsed as %f\n", parsed_64);
printf("as raw uint64_t, parsed = %llu, expected = %llu\n", (unsigned long long)float64_parsed, (unsigned long long)float64);
printf("fesetround: %s\n", round_name(d));
fclose(file);
fesetround(FE_TONEAREST);
return 0;
}
number++;
}
}
printf("checked %zu values\n", number);
fclose(file);
} else {
printf("Could not read %s\n", file_name);
fesetround(FE_TONEAREST);
return 0;
}
}
fesetround(FE_TONEAREST);
return 1;
}

int main(void) {
DIR* dir = opendir(SUPPLEMENTAL_TEST_DATA_DIR);
if (!dir) {
fprintf(stderr, "Could not open directory %s: %s\n", SUPPLEMENTAL_TEST_DATA_DIR, strerror(errno));
return 1;
}
struct dirent* entry;
int all_passed = 1;
while ((entry = readdir(dir)) != NULL) {
if (entry->d_name[0] == '.') continue; // skip hidden
char file_path[1024];
snprintf(file_path, sizeof(file_path), "%s/%s", SUPPLEMENTAL_TEST_DATA_DIR, entry->d_name);
struct stat st;
if (stat(file_path, &st) == 0 && S_ISREG(st.st_mode)) {
printf("Testing file: %s\n", file_path);
if (!check_file(file_path)) {
all_passed = 0;
}
}
}
closedir(dir);
return all_passed ? EXIT_SUCCESS : EXIT_FAILURE;
}
2 changes: 1 addition & 1 deletion test_src/test.c
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ char *double_to_string(double d, char *buffer) {
return buffer + written;
}

inline ffc_outcome parse_outcome(uint64_t len, const char* outcome_text) {
inline static ffc_outcome parse_outcome(uint64_t len, const char* outcome_text) {
static const struct { const char *name; ffc_outcome val; } map[] = {
{"ok", FFC_OUTCOME_OK},
{"out_of_range", FFC_OUTCOME_OUT_OF_RANGE},
Expand Down
Loading