string: add nstr2i and nstr2u

This commit is contained in:
anna 2021-07-31 00:59:20 +02:00
parent 57b24014dc
commit 026bfd0b39
Signed by: fef
GPG key ID: EC22E476DC2D3D84
6 changed files with 715 additions and 1 deletions

View file

@ -131,6 +131,76 @@ nstr_t *i2nstr(i64 i, int radix, error *err);
*/
nstr_t *u2nstr(u64 u, int radix, error *err);
/**
* @brief Parse a numerical string to a signed integer.
*
* The string is parsed case-insensitive and may be prefixed with a single
* `+` or `-` sign. If `radix` is 0, the first two characters (after the
* sign, if present) determine the base:
*
* - If the string begins with `0b` or `0B`, `radix` is 2.
* - If the string begins with `0o` or `0O`, `radix` is 8.
* - If the string begins with `0x` or `0X`, `radix` is 16.
* - In all other cases, `radix` is 10, even if there are leading zeroes.
*
* If the number contains illegal characters or cannot be stored in an `i64`,
* `s` is nil, or `radix` is outside the allowed range, an error is yeeted.
*
* @param s String to parse
* @param radix Base of the numerical system to interpret the string as.
* Must be either within 2~36, or 0 to determine automatically (see above).
* @param err Error pointer
* @returns The parsed integer, unless an error occurred
*/
i64 nstr2i(const nstr_t *s, int radix, error *err);
/**
* @brief The same as `nstr2i()`, but `nput()` is called on `s` afterwards.
*
* @param s String to parse
* @param radix Base of the numerical system to interpret the string as.
* Must be either within 2~36, or 0 to determine automatically.
* @param err Error pointer
* @returns The parsed integer, unless an error occurred
* @see nstr2i
*/
i64 nstr2i_put(nstr_t *s, int radix, error *err);
/**
* @brief Parse a numerical string to an unsigned integer.
*
* The string is parsed case-insensitive and may be prefixed with a single
* `+` (no `-`) sign. If `radix` is 0, the first two characters (after the
* sign, if present) determine the base:
*
* - If the string begins with `0b` or `0B`, `radix` is 2.
* - If the string begins with `0o` or `0O`, `radix` is 8.
* - If the string begins with `0x` or `0X`, `radix` is 16.
* - In all other cases, `radix` is 10, even if there are leading zeroes.
*
* If the number contains illegal characters or cannot be stored in an `u64`,
* `s` is nil, or `radix` is outside the allowed range, an error is yeeted.
*
* @param s String to parse
* @param radix Base of the numerical system to interpret the string as.
* Must be either within 2~36, or 0 to determine automatically (see above).
* @param err Error pointer
* @returns The parsed integer, unless an error occurred
*/
u64 nstr2u(const nstr_t *s, int radix, error *err);
/**
* @brief The same as `nstr2u()`, but `nput()` is called on `s` afterwards.
*
* @param s String to parse
* @param radix Base of the numerical system to interpret the string as.
* Must be either within 2~36, or 0 to determine automatically (see above).
* @param err Error pointer
* @returns The parsed integer, unless an error occurred
* @see nstr2u
*/
u64 nstr2u_put(nstr_t *s, int radix, error *err);
/**
* @brief Duplicate a string.
*

187
src/string/nstr2x.c Normal file
View file

@ -0,0 +1,187 @@
/* See the end of this file for copyright and license terms. */
#include <errno.h>
#include "neo/_error.h"
#include "neo/_nref.h"
#include "neo/_nstr.h"
#include "neo/_stddef.h"
#include "neo/_types.h"
static inline u64 nstr2u_unsafe(const char *s, unsigned int radix, error *err)
{
u64 num = 0;
while (*s != '\0') {
u64 tmp = num;
num *= radix;
if (num / radix != tmp) { /* overflow */
yeet(err, ERANGE, "Number out of range");
return 0xffffffffffffffff;
}
u8 digit = 0;
if (*s >= '0' && *s <= '9')
digit = *s++ - '0';
else if (*s >= 'A' && *s <= 'Z')
digit = *s++ - 'A' + 10;
else if (*s >= 'a' && *s <= 'z')
digit = *s++ - 'a' + 10;
else
break;
if (digit >= radix) {
s--;
break;
}
tmp = num;
num += digit;
if (tmp > num) { /* overflow */
yeet(err, ERANGE, "Number out of range");
return 0xffffffffffffffff;
}
}
if (*s != '\0') {
yeet(err, EINVAL, "'%c' is not a base %u digit", *s, radix);
return 0;
} else {
neat(err);
return num;
}
}
static const char *strip_prefix_and_guess_radix(int *radix, const char *s, error *err)
{
if (*radix < 0 || *radix > 36 || *radix == 1) {
yeet(err, EINVAL, "Numerical base out of range");
return nil;
}
int guessed_radix = 10;
if (s[0] == '0') {
switch (s[1]) {
case 'x':
case 'X':
guessed_radix = 16;
s += 2;
break;
case 'o':
case 'O':
guessed_radix = 8;
s += 2;
break;
case 'b':
case 'B':
s += 2;
guessed_radix = 2;
break;
}
}
if (*radix == 0)
*radix = guessed_radix;
if (guessed_radix != 10 && *radix != guessed_radix) {
yeet(err, EINVAL, "Numerical base does not match number prefix");
return nil;
}
neat(err);
return s;
}
u64 nstr2u(const nstr_t *s, int radix, error *err)
{
if (s == nil) {
yeet(err, EFAULT, "String is nil");
return 0;
}
const char *raw = s->_data;
if (*raw == '+')
raw++;
raw = strip_prefix_and_guess_radix(&radix, raw, err);
catch(err) {
return 0;
}
return nstr2u_unsafe(raw, (unsigned int)radix, err);
}
i64 nstr2i(const nstr_t *s, int radix, error *err)
{
if (s == nil) {
yeet(err, EFAULT, "String is nil");
return 0;
}
const char *raw = s->_data;
bool negative = *raw == '-';
if (*raw == '+' || *raw == '-')
raw++;
raw = strip_prefix_and_guess_radix(&radix, raw, err);
catch(err) {
return 0;
}
u64 u = nstr2u_unsafe(raw, (unsigned int)radix, err);
catch(err) {
return u;
}
if (u == 0)
negative = false;
if (u - (u64)negative >= ((u64)1 << 63)) {
yeet(err, ERANGE, "Number out of range");
i64 ret = ((u64)1 << 63) - 1;
ret += (i64)negative; /* overflow to min i64 if negative */
return ret;
}
if (negative)
return -(i64)u;
else
return (i64)u;
}
u64 nstr2u_put(nstr_t *s, int radix, error *err)
{
u64 ret = nstr2u(s, radix, err);
catch(err) {
return 0;
}
nput(s);
return ret;
}
i64 nstr2i_put(nstr_t *s, int radix, error *err)
{
i64 ret = nstr2i(s, radix, err);
catch(err) {
return 0;
}
nput(s);
return ret;
}
/*
* This file is part of libneo.
* Copyright (c) 2021 Fefie <owo@fef.moe>.
*
* libneo is non-violent software: you may only use, redistribute,
* and/or modify it under the terms of the CNPLv6+ as found in
* the LICENSE file in the source code root directory or at
* <https://git.pixie.town/thufie/CNPL>.
*
* libneo comes with ABSOLUTELY NO WARRANTY, to the extent
* permitted by applicable law. See the CNPLv6+ for details.
*/

View file

@ -1,11 +1,12 @@
target_sources(neo PRIVATE
./string/leftpad.c
./string/nstr.c
./string/nstr2x.c
./string/nstrcat.c
./string/nstrcmp.c
./string/nstrdup.c
./string/nstrmul.c
./string/nstrtrim.c
./string/leftpad.c
./string/utf.c
./string/x2nstr.c
)

239
test/string/nstr2i.cpp Normal file
View file

@ -0,0 +1,239 @@
/* See the end of this file for copyright and license terms. */
#include <catch2/catch.hpp>
#include <errno.h>
#include <neo.h>
extern "C" struct nstr2i_positive_case {
const char *s;
i64 i;
int radix;
};
TEST_CASE( "nstr2i: Parse numbers", "[string/nstr2x.c]" )
{
struct nstr2i_positive_case positives[] = {
{"0", 0, 2 },
{"0", 0, 36 },
{"1y2p0ij32e8e7", 0x7fffffffffffffff, 36 },
{"-1y2p0ij32e8e8", (i64)0x8000000000000000, 36 },
{"1Y2P0IJ32E8E7", 0x7fffffffffffffff, 36 },
{"-1Y2P0IJ32E8E8", (i64)0x8000000000000000, 36 },
{"0x7fffffffffffffff", 0x7fffffffffffffff, 0 },
{"-0x8000000000000000", (i64)0x8000000000000000, 0 },
{"69420", 69420, 0 },
{"+69420", 69420, 0 },
{"-69420", -69420, 0 },
{"0xff", 0xff, 0 },
{"0XFF", 0xff, 0 },
{"+0xff", 0xff, 0 },
{"+0XFF", 0xff, 0 },
{"-0xff", -0xff, 0 },
{"-0XFF", -0xff, 0 },
{"0b11111111", 0xff, 0 },
{"0B11111111", 0xff, 0 },
{"0o77777777", 077777777, 0 },
{"0O77777777", 077777777, 0 },
{"069420", 69420, 0 }
};
struct nstr2i_positive_case *current_case = &positives[0];
do {
error err;
nstr_t *s = nstr(current_case->s, nil);
i64 i = nstr2i(s, current_case->radix, &err);
REQUIRE( i == current_case->i );
REQUIRE( errnum(&err) == 0 );
} while (++current_case < &positives[0] + sizeof(positives) / sizeof(positives[0]));
}
TEST_CASE( "nstr2i: Error if number too low", "[string/nstr2x.c]" )
{
error err;
nstr_t *s = nstr("-8000000000000001", nil);
i64 i = nstr2i(s, 16, &err);
nstr_t *expected_msg = nstr("Number out of range", nil);
nstr_t *actual_msg = errmsg(&err);
REQUIRE( i == -0x8000000000000000 );
REQUIRE( errnum(&err) == ERANGE );
REQUIRE( nstreq(expected_msg, actual_msg, nil) );
nput(expected_msg);
nput(actual_msg);
nput(s);
}
TEST_CASE( "nstr2i: Error if number too high", "[string/nstr2x.c]" )
{
error err;
nstr_t *s = nstr("8000000000000000", nil);
i64 i = nstr2i(s, 16, &err);
nstr_t *expected_msg = nstr("Number out of range", nil);
nstr_t *actual_msg = errmsg(&err);
REQUIRE( i == 0x7fffffffffffffff );
REQUIRE( errnum(&err) == ERANGE );
REQUIRE( nstreq(expected_msg, actual_msg, nil) );
nput(expected_msg);
nput(actual_msg);
nput(s);
}
TEST_CASE( "nstr2i: Error if base too low", "[string/nstr2x.c]" )
{
error err;
nstr_t *s = nstr("420", nil);
i64 i = nstr2i(s, -1, &err);
nstr_t *expected_msg = nstr("Numerical base out of range", nil);
nstr_t *actual_msg = errmsg(&err);
REQUIRE( i == 0 );
REQUIRE( nstreq(expected_msg, actual_msg, nil) );
REQUIRE( errnum(&err) == EINVAL );
nput(expected_msg);
nput(actual_msg);
nput(s);
errput(&err);
}
TEST_CASE( "nstr2i: Error if base is 1", "[string/nstr2x.c]" )
{
error err;
nstr_t *s = nstr("420", nil);
i64 i = nstr2i(s, 1, &err);
nstr_t *expected_msg = nstr("Numerical base out of range", nil);
nstr_t *actual_msg = errmsg(&err);
REQUIRE( i == 0 );
REQUIRE( nstreq(expected_msg, actual_msg, nil) );
REQUIRE( errnum(&err) == EINVAL );
nput(expected_msg);
nput(actual_msg);
nput(s);
errput(&err);
}
TEST_CASE( "nstr2i: Error if base too high", "[string/nstr2x.c]" )
{
error err;
nstr_t *s = nstr("420", nil);
i64 i = nstr2i(s, 37, &err);
nstr_t *expected_msg = nstr("Numerical base out of range", nil);
nstr_t *actual_msg = errmsg(&err);
REQUIRE( i == 0 );
REQUIRE( nstreq(expected_msg, actual_msg, nil) );
REQUIRE( errnum(&err) == EINVAL );
nput(expected_msg);
nput(actual_msg);
nput(s);
errput(&err);
}
TEST_CASE( "nstr2i: Error if character not part of numerical system", "[string/nstr2x.c]" )
{
error err;
nstr_t *s = nstr("012", nil);
i64 i = nstr2i(s, 2, &err);
nstr_t *expected_msg = nstr("'2' is not a base 2 digit", nil);
nstr_t *actual_msg = errmsg(&err);
REQUIRE( i == 0 );
REQUIRE( nstreq(expected_msg, actual_msg, nil) );
REQUIRE( errnum(&err) == EINVAL );
nput(expected_msg);
nput(actual_msg);
nput(s);
errput(&err);
}
TEST_CASE( "nstr2i: Error if non-digit character", "[string/nstr2x.c]" )
{
error err;
nstr_t *s = nstr("0,12", nil);
i64 i = nstr2i(s, 2, &err);
nstr_t *expected_msg = nstr("',' is not a base 2 digit", nil);
nstr_t *actual_msg = errmsg(&err);
REQUIRE( i == 0 );
REQUIRE( nstreq(expected_msg, actual_msg, nil) );
REQUIRE( errnum(&err) == EINVAL );
nput(expected_msg);
nput(actual_msg);
nput(s);
errput(&err);
}
TEST_CASE( "nstr2i: Error if string is nil", "[string/nstr2x.c]" )
{
error err;
i64 i = nstr2i(nil, 2, &err);
nstr_t *expected_msg = nstr("String is nil", nil);
nstr_t *actual_msg = errmsg(&err);
REQUIRE( i == 0 );
REQUIRE( nstreq(expected_msg, actual_msg, nil) );
REQUIRE( errnum(&err) == EFAULT );
nput(expected_msg);
errput(&err);
}
TEST_CASE( "nstr2i_put: Decrement refcount", "[string/nstr2x.c]" )
{
error err;
nstr_t *s = nstr("420", nil);
nget(s);
i64 i = nstr2i_put(s, 10, &err);
REQUIRE( i == 420 );
REQUIRE( nref_count(s) == 1 );
nput(s);
}
TEST_CASE( "nstr2i_put: Error if string is nil", "[string/nstr2x.c]" )
{
error err;
i64 i = nstr2i_put(nil, 2, &err);
nstr_t *expected_msg = nstr("String is nil", nil);
nstr_t *actual_msg = errmsg(&err);
REQUIRE( i == 0 );
REQUIRE( nstreq(expected_msg, actual_msg, nil) );
REQUIRE( errnum(&err) == EFAULT );
nput(expected_msg);
errput(&err);
}
/*
* This file is part of libneo.
* Copyright (c) 2021 Fefie <owo@fef.moe>.
*
* libneo is non-violent software: you may only use, redistribute,
* and/or modify it under the terms of the CNPLv6+ as found in
* the LICENSE file in the source code root directory or at
* <https://git.pixie.town/thufie/CNPL>.
*
* libneo comes with ABSOLUTELY NO WARRANTY, to the extent
* permitted by applicable law. See the CNPLv6+ for details.
*/

215
test/string/nstr2u.cpp Normal file
View file

@ -0,0 +1,215 @@
/* See the end of this file for copyright and license terms. */
#include <catch2/catch.hpp>
#include <errno.h>
#include <neo.h>
extern "C" struct nstr2u_positive_case {
const char *s;
u64 u;
int radix;
};
TEST_CASE( "nstr2u: Parse numbers", "[string/nstr2x.c]" )
{
struct nstr2u_positive_case positives[] = {
{"0", 0, 2 },
{"0", 0, 36 },
{"3w5e11264sgsf", 0xffffffffffffffff, 36 },
{"3W5E11264SGSF", 0xffffffffffffffff, 36 },
{"0xffffffffffffffff", 0xffffffffffffffff, 0 },
{"69420", 69420, 0 },
{"+69420", 69420, 0 },
{"0xff", 0xff, 0 },
{"0XFF", 0xff, 0 },
{"+0xff", 0xff, 0 },
{"+0XFF", 0xff, 0 },
{"0b11111111", 0xff, 0 },
{"0B11111111", 0xff, 0 },
{"0o77777777", 077777777, 0 },
{"0O77777777", 077777777, 0 },
{"069420", 69420, 0 }
};
struct nstr2u_positive_case *current_case = &positives[0];
do {
error err;
nstr_t *s = nstr(current_case->s, nil);
u64 u = nstr2u(s, current_case->radix, &err);
REQUIRE( u == current_case->u );
REQUIRE( errnum(&err) == 0 );
} while (++current_case < &positives[0] + sizeof(positives) / sizeof(positives[0]));
}
TEST_CASE( "nstr2u: Error if number too high", "[string/nstr2x.c]" )
{
error err;
nstr_t *s = nstr("10000000000000000", nil);
u64 u = nstr2u(s, 16, &err);
nstr_t *expected_msg = nstr("Number out of range", nil);
nstr_t *actual_msg = errmsg(&err);
REQUIRE( u == 0xffffffffffffffff );
REQUIRE( errnum(&err) == ERANGE );
REQUIRE( nstreq(expected_msg, actual_msg, nil) );
nput(expected_msg);
nput(actual_msg);
nput(s);
}
TEST_CASE( "nstr2u: Error if base too low", "[string/nstr2x.c]" )
{
error err;
nstr_t *s = nstr("420", nil);
u64 u = nstr2u(s, -1, &err);
nstr_t *expected_msg = nstr("Numerical base out of range", nil);
nstr_t *actual_msg = errmsg(&err);
REQUIRE( u == 0 );
REQUIRE( nstreq(expected_msg, actual_msg, nil) );
REQUIRE( errnum(&err) == EINVAL );
nput(expected_msg);
nput(actual_msg);
nput(s);
errput(&err);
}
TEST_CASE( "nstr2u: Error if base is 1", "[string/nstr2x.c]" )
{
error err;
nstr_t *s = nstr("420", nil);
u64 u = nstr2u(s, 1, &err);
nstr_t *expected_msg = nstr("Numerical base out of range", nil);
nstr_t *actual_msg = errmsg(&err);
REQUIRE( u == 0 );
REQUIRE( nstreq(expected_msg, actual_msg, nil) );
REQUIRE( errnum(&err) == EINVAL );
nput(expected_msg);
nput(actual_msg);
nput(s);
errput(&err);
}
TEST_CASE( "nstr2u: Error if base too high", "[string/nstr2x.c]" )
{
error err;
nstr_t *s = nstr("420", nil);
u64 u = nstr2u(s, 37, &err);
nstr_t *expected_msg = nstr("Numerical base out of range", nil);
nstr_t *actual_msg = errmsg(&err);
REQUIRE( u == 0 );
REQUIRE( nstreq(expected_msg, actual_msg, nil) );
REQUIRE( errnum(&err) == EINVAL );
nput(expected_msg);
nput(actual_msg);
nput(s);
errput(&err);
}
TEST_CASE( "nstr2u: Error if character not part of numerical system", "[string/nstr2x.c]" )
{
error err;
nstr_t *s = nstr("012", nil);
u64 u = nstr2u(s, 2, &err);
nstr_t *expected_msg = nstr("'2' is not a base 2 digit", nil);
nstr_t *actual_msg = errmsg(&err);
REQUIRE( u == 0 );
REQUIRE( nstreq(expected_msg, actual_msg, nil) );
REQUIRE( errnum(&err) == EINVAL );
nput(expected_msg);
nput(actual_msg);
nput(s);
errput(&err);
}
TEST_CASE( "nstr2u: Error if non-digit character", "[string/nstr2x.c]" )
{
error err;
nstr_t *s = nstr("0,12", nil);
u64 u = nstr2u(s, 2, &err);
nstr_t *expected_msg = nstr("',' is not a base 2 digit", nil);
nstr_t *actual_msg = errmsg(&err);
REQUIRE( u == 0 );
REQUIRE( nstreq(expected_msg, actual_msg, nil) );
REQUIRE( errnum(&err) == EINVAL );
nput(expected_msg);
nput(actual_msg);
nput(s);
errput(&err);
}
TEST_CASE( "nstr2u: Error if string is nil", "[string/nstr2x.c]" )
{
error err;
u64 u = nstr2u(nil, 2, &err);
nstr_t *expected_msg = nstr("String is nil", nil);
nstr_t *actual_msg = errmsg(&err);
REQUIRE( u == 0 );
REQUIRE( nstreq(expected_msg, actual_msg, nil) );
REQUIRE( errnum(&err) == EFAULT );
nput(expected_msg);
errput(&err);
}
TEST_CASE( "nstr2u_put: Decrement refcount", "[string/nstr2x.c]" )
{
error err;
nstr_t *s = nstr("420", nil);
nget(s);
u64 u = nstr2u_put(s, 10, &err);
REQUIRE( u == 420 );
REQUIRE( nref_count(s) == 1 );
nput(s);
}
TEST_CASE( "nstr2u_put: Error if string is nil", "[string/nstr2x.c]" )
{
error err;
u64 u = nstr2u_put(nil, 2, &err);
nstr_t *expected_msg = nstr("String is nil", nil);
nstr_t *actual_msg = errmsg(&err);
REQUIRE( u == 0 );
REQUIRE( nstreq(expected_msg, actual_msg, nil) );
REQUIRE( errnum(&err) == EFAULT );
nput(expected_msg);
errput(&err);
}
/*
* This file is part of libneo.
* Copyright (c) 2021 Fefie <owo@fef.moe>.
*
* libneo is non-violent software: you may only use, redistribute,
* and/or modify it under the terms of the CNPLv6+ as found in
* the LICENSE file in the source code root directory or at
* <https://git.pixie.town/thufie/CNPL>.
*
* libneo comes with ABSOLUTELY NO WARRANTY, to the extent
* permitted by applicable law. See the CNPLv6+ for details.
*/

View file

@ -6,6 +6,8 @@ target_sources(neo_test PRIVATE
string/i2nstr.cpp
string/leftpad.cpp
string/nstr.cpp
string/nstr2i.cpp
string/nstr2u.cpp
string/nstrcat.cpp
string/nstrcmp.cpp
string/nstrdup.cpp