Skip to content

Commit

Permalink
fix(conversion): fix conversion operations (#26)
Browse files Browse the repository at this point in the history
* support conversions for float types

* add conversion example

* simplify README & ready to release 0.1
  • Loading branch information
guuzaa committed Mar 24, 2024
1 parent a22bda7 commit 52c9be7
Show file tree
Hide file tree
Showing 10 changed files with 169 additions and 14 deletions.
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.10)

set(version 0.0.3)
set(version 0.1.0)

project(numbers VERSION ${version} LANGUAGES CXX)

Expand Down
16 changes: 7 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,21 @@ numbers
[![CMake CI Matrix](https://github.com/guuzaa/numbers/actions/workflows/cmake.yml/badge.svg?branch=main)](https://github.com/guuzaa/numbers/actions/workflows/cmake.yml)
![language c++17](https://img.shields.io/badge/Language-C++17-red)
[![license mit](https://img.shields.io/badge/License-MIT-pink)](https://github.com/guuzaa/numbers/blob/main/LICENSE.txt)
![linux](https://img.shields.io/badge/OS-Linux-blue)
![macOS](https://img.shields.io/badge/OS-macOS-blue)
![windows](https://img.shields.io/badge/OS-Windows-blue)

> [!IMPORTANT]
> This project is in the early stages of development. The codebase is subject to significant changes and reorganization. Expect breaking changes as we refine the architecture, fix bugs, and implement new features.

`numbers` is a library for C++17 and later versions that handles integer overflow similar to Rust. It simplifies integer overflow situations.

## Features

- **Full Control** over handling integer overflow

- **Support for Multiple Toolchains**: GCC, Clang, MSVC

- Same as **Primitive Types** (WIP)

- **Support Integers**: i8, i16, i32, i64, u8, u16, u32, u64, even i128 & u128 (not ready yet)
- **Support for Various Integer Type**: i8, i16, i32, i64, u8, u16, u32, u64, even i128 & u128

## Usage
<details>
<summary>Usage</summary>

### operator +
```c++
Expand Down Expand Up @@ -69,10 +66,11 @@ std::cout << "a= " << a << ", b= " << b << '\n';
numbers::i64 ret = a.saturating_mul(b);
std::cout << ret << '\n';
```
</details>

## Contribute

We welcome contributions, but please be aware that the project's design and conventions are still evolving. If you'd like to contribute, it's a good idea to discuss your plans with the project maintainers before starting work.
If you'd like to contribute, it's a good idea to discuss your plans with the project maintainers before starting work.

For the latest updates and discussions, please see our [issues](https://github.com/guuzaa/numbers/issues) and [pull requests](https://github.com/guuzaa/numbers/pulls).

Expand Down
19 changes: 19 additions & 0 deletions examples/conversion.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#include <iostream>
#include "numbers.h"

int main(int argc, char const *argv[]) {
std::cout << "==== conversion example ==== \n";
numbers::u128 a = 35.6;
numbers::i128 b = 35.6;
numbers::i64 c = 135.6;
std::cout << "a = " << a << ", b = " << b << ", c = " << c << '\n';
// do some conversions
a = static_cast<numbers::u128>(c);
b = static_cast<numbers::i128>(c);
std::cout << "a = " << a << ", b = " << b << ", c = " << c << '\n';

b = 345;
c = static_cast<numbers::i64>(b);
std::cout << "a = " << a << ", b = " << b << ", c = " << c << '\n';
return 0;
}
File renamed without changes.
3 changes: 0 additions & 3 deletions src/include/int128.hh
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,6 @@ class int128 {
constexpr explicit operator unsigned __int128() const;
#endif

// TODO unimplemented!
explicit operator float() const;
explicit operator double() const;
explicit operator long double() const;
Expand Down Expand Up @@ -724,8 +723,6 @@ inline uint128 operator*(uint128 lhs, uint128 rhs) {
#endif
}

// TODO implement division and modulo

// Increment/decrement operators
inline uint128 uint128::operator++(int) {
uint128 tmp(*this);
Expand Down
23 changes: 23 additions & 0 deletions src/include/int128_no_intrinstic.inc
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,29 @@ constexpr int128::operator long long() const { return int128_internal::BitCastTo

constexpr int128::operator unsigned long long() const { return static_cast<unsigned long long>(lo_); }

inline int128::operator float() const {
// We must convert the absolute value and then negate as needed, because
// floating point types are typically sign-magnitude. Otherwise, the
// difference between the high and low 64 bits when interpreted as two's
// complement overwhelms the precision of the mantissa.
//
// Also check to make sure we don't negate Int128Min()
return hi_ < 0 && *this != MIN ? -static_cast<float>(-*this)
: static_cast<float>(lo_) + std::ldexp(static_cast<float>(hi_), 64);
}

inline int128::operator double() const {
// See comment in int128::operator float() above.
return hi_ < 0 && *this != MIN ? -static_cast<double>(-*this)
: static_cast<double>(lo_) + std::ldexp(static_cast<double>(hi_), 64);
}

inline int128::operator long double() const {
// See comment in int128::operator float() above.
return hi_ < 0 && *this != MIN ? -static_cast<long double>(-*this)
: static_cast<long double>(lo_) + std::ldexp(static_cast<long double>(hi_), 64);
}

// Comparison operators
constexpr bool operator==(int128 lhs, int128 rhs) {
return (int128_low64(lhs) == int128_low64(rhs) && int128_high64(lhs) == int128_high64(rhs));
Expand Down
6 changes: 5 additions & 1 deletion src/include/integer.hh
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@ class Integer {
constexpr Integer() noexcept : num_{} {}

template <typename U, typename = std::enable_if_t<std::is_convertible_v<U, T>>>
Integer(U num) noexcept : num_{static_cast<T>(num)} {}
Integer(U num) : num_{static_cast<T>(num)} {}

Integer(float num) noexcept : num_{static_cast<T>(num)} {}
Integer(double num) noexcept : num_{static_cast<T>(num)} {}
Integer(long double num) noexcept : num_{static_cast<T>(num)} {}

constexpr Integer operator+(Integer<T> other) const noexcept(false) {
if (add_overflow(num_, other.num_)) {
Expand Down
4 changes: 4 additions & 0 deletions src/include/uinteger.hh
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ class Uinteger {
template <typename U, typename = std::enable_if_t<std::is_convertible_v<U, T>>>
Uinteger(U num) noexcept : num_{static_cast<T>(num)} {}

Uinteger(float num) noexcept : num_{static_cast<T>(num)} {}
Uinteger(double num) noexcept : num_{static_cast<T>(num)} {}
Uinteger(long double num) noexcept : num_{static_cast<T>(num)} {}

constexpr Uinteger operator+(const Uinteger<T> &other) const noexcept(false) {
if (add_overflow(num_, other.num_)) {
throw std::runtime_error("add overflow");
Expand Down
39 changes: 39 additions & 0 deletions src/numbers/int128.cc
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,26 @@ std::string uint128_to_formatted_string(uint128 v, std::ios_base::fmtflags flags
return os.str();
}

template <typename T>
uint128 make_uint128_from_float(T v) {
static_assert(std::is_floating_point<T>::value, "");
// Undefined behavior if v is NaN or cannot fit into uint128
assert(std::isfinite(v) && v > -1 &&
(std::numeric_limits<T>::max_exponent <= 128 || v < std::ldexp(static_cast<T>(1), 128)));

if (v >= std::ldexp(static_cast<T>(1), 64)) {
uint64_t hi = static_cast<uint64_t>(std::ldexp(v, -64));
uint64_t lo = static_cast<uint64_t>(v - std::ldexp(static_cast<T>(hi), 64));
return make_uint128(hi, lo);
}
return make_uint128(0, static_cast<uint64_t>(v));
}
} // namespace

uint128::uint128(float v) : uint128(make_uint128_from_float(v)) {}
uint128::uint128(double v) : uint128(make_uint128_from_float(v)) {}
uint128::uint128(long double v) : uint128(make_uint128_from_float(v)) {}

} // namespace numbers

namespace numbers {
Expand Down Expand Up @@ -236,4 +254,25 @@ std::ostream &operator<<(std::ostream &os, int128 v) {

std::string int128::to_string() const { return uint128_to_formatted_string(*this, std::ios_base::dec); }

#ifndef NUMBERS_HAVE_INTRINSTIC_INT128
namespace {

template <typename T>
int128 make_int128_from_float(T v) {
// Conversion when v is NaN or cannot fit into int128 would be undefined
// behavior if using an intrinsic 128-bit integer.
assert(std::isfinite(v) && (std::numeric_limits<T>::max_exponent <= 127 ||
(v >= -std::ldexp(static_cast<T>(1), 127) && v < std::ldexp(static_cast<T>(1), 127))));
uint128 result = v < 0 ? -make_uint128_from_float(-v) : make_uint128_from_float(v);
return make_int128(int128_internal::BitCastToSigned(uint128_high64(result)), uint128_low64(result));
}

} // namespace

int128::int128(float v) : int128(make_int128_from_float(v)) {}
int128::int128(double v) : int128(make_int128_from_float(v)) {}
int128::int128(long double v) : int128(make_int128_from_float(v)) {}

#endif

} // namespace numbers
71 changes: 71 additions & 0 deletions tests/integer/int128.test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,77 @@ TYPED_TEST(Int128TraitsTest, ConstructAssignTest) {
"TypeParam must not be assignable from numbers::int128");
}

typedef ::testing::Types<float, double, long double> FloatTypes;

template <typename T>
class Int128FloatConversionTest : public ::testing::Test {};

TYPED_TEST_SUITE(Int128FloatConversionTest, FloatTypes);

TYPED_TEST(Int128FloatConversionTest, ConstructAndCastTest) {
// Conversions where the floating point values should be exactly the same.
for (int i = 0; i < 110; ++i) {
SCOPED_TRACE(::testing::Message() << "i = " << i);
TypeParam float_value = std::ldexp(static_cast<TypeParam>(0x9f4b), i);
int128 int_value = int128(0x9f4b) << i;

EXPECT_EQ(float_value, static_cast<TypeParam>(int_value));
EXPECT_EQ(-float_value, static_cast<TypeParam>(-int_value));
EXPECT_EQ(int_value, int128(float_value));
EXPECT_EQ(-int_value, int128(-float_value));
}

// Round trip conversions with a small sample of randomly generated uint64_t
// values (less than int64_t max so that value * 2^64 fits into int128).
uint64_t values[] = {0x6d4493c24fb86199, 0x26ecd65e4cb359b5, 0x2c43417433ba3fd1, 0x3b573ec669df6b55,
0x1c751e55a29f4f0f};
for (uint64_t value : values) {
for (int i = 0; i <= 64; ++i) {
SCOPED_TRACE(::testing::Message() << "value = " << value << "; i = " << i);

TypeParam fvalue = std::ldexp(static_cast<TypeParam>(value), i);
EXPECT_DOUBLE_EQ(fvalue, static_cast<TypeParam>(int128(fvalue)));
EXPECT_DOUBLE_EQ(-fvalue, static_cast<TypeParam>(-int128(fvalue)));
EXPECT_DOUBLE_EQ(-fvalue, static_cast<TypeParam>(int128(-fvalue)));
EXPECT_DOUBLE_EQ(fvalue, static_cast<TypeParam>(-int128(-fvalue)));
}
}

// Round trip conversions with a small sample of random large positive values.
int128 large_values[] = {
make_int128(0x5b0640d96c7b3d9f, 0xb7a7089e51d18622), make_int128(0x34bed042c6f65270, 0x74b236570669a089),
make_int128(0x43debc9e6da12724, 0xf7f0f83da686797d), make_int128(0x71e8d483be4e5589, 0x75c3f96fb00752b6)};
for (int128 value : large_values) {
// Make value have as many significant bits as can be represented by
// the mantissa, also making sure the highest and lowest bit in the range
// are set.
value >>= (127 - std::numeric_limits<TypeParam>::digits);
value |= int128(1) << (std::numeric_limits<TypeParam>::digits - 1);
value |= 1;
for (int i = 0; i < 127 - std::numeric_limits<TypeParam>::digits; ++i) {
int128 int_value = value << i;
EXPECT_EQ(int_value, static_cast<int128>(static_cast<TypeParam>(int_value)));
EXPECT_EQ(-int_value, static_cast<int128>(static_cast<TypeParam>(-int_value)));
}
}

// Small sample of checks that rounding is toward zero
EXPECT_EQ(0, int128(TypeParam(0.01)));
EXPECT_EQ(17, int128(TypeParam(17.8)));
EXPECT_EQ(0, int128(TypeParam(-0.803)));
EXPECT_EQ(-53, int128(TypeParam(-53.1)));
EXPECT_EQ(0, int128(TypeParam(0.5)));
EXPECT_EQ(0, int128(TypeParam(-0.05)));
TypeParam just_lt_one = std::nexttoward(TypeParam(1), TypeParam(0));
EXPECT_EQ(0, int128(just_lt_one));
TypeParam just_gt_minus_one = std::nexttoward(TypeParam(-1), TypeParam(0));
EXPECT_EQ(0, int128(just_gt_minus_one));

// Check limits
EXPECT_DOUBLE_EQ(std::ldexp(static_cast<TypeParam>(1), 127), static_cast<TypeParam>(int128_max()));
EXPECT_DOUBLE_EQ(-std::ldexp(static_cast<TypeParam>(1), 127), static_cast<TypeParam>(int128_min()));
}

TEST(Int128Test, BoolConversion) {
EXPECT_FALSE(int128(0));
for (int i = 0; i < 64; ++i) {
Expand Down

0 comments on commit 52c9be7

Please sign in to comment.