paupino / rust-decimal

Decimal number implementation written in pure Rust suitable for financial and fixed-precision calculations.
https://docs.rs/rust_decimal/
MIT License
1.01k stars 183 forks source link

Add support for use num_traits::cast::NumCast #573

Open MitchellNeill opened 1 year ago

MitchellNeill commented 1 year ago

Disclaimer I am pretty new to rust

I was wondering if support could be added for the num_traits::cast::NumCast

The trait is defined as part of this file: https://docs.rs/num-traits/latest/src/num_traits/cast.rs.html#721

I would like to implement it as I wish to use Decimal with the geo library: geo::Coord::::{x: 1., y:1.} however it only accepts types which implement numcast.

I had a go at implementing how I think it should look , I think there are three high level options:

1) Figure out the input type and call the appropriate conversion method (requires figuring out input type) 2) Always convert input to an f64 and convert f64 to a Decimal (has some data loss when input is i128) 3) Use some magic I am unaware of

I was wondering if I could get some pointers, or help implementing this.

In regards to figuring out the type, It looks like the standard approach here will not work, due to lifetimes, but I am not very good at using them, so there may be a work around. I have included code for this approach at the bottom of this pitiful cry for help. https://doc.rust-lang.org/std/any/index.html

I found a hacky looking, but powerful(?) approach here

which I modified and included below, but the method I use to check types is maybe dangerous:

use num_traits::cast::{FromPrimitive, NumCast, ToPrimitive};
use crate::prelude::Decimal;
use std::any::type_name;

// By directly calling `type_name` here we guarantee that the names
// remain "up to date".
const I8_NAME: &str = type_name::<i8>();
const I16_NAME: &str = type_name::<i16>();
const I32_NAME: &str = type_name::<i32>();
const I64_NAME: &str = type_name::<i64>();
const I128_NAME: &str = type_name::<i128>();

const ISIZE_NAME: &str = type_name::<isize>();

const F32_NAME: &str = type_name::<f32>();
const F64_NAME: &str = type_name::<f64>();

const U8_NAME: &str = type_name::<u8>();
const U16_NAME: &str = type_name::<u16>();
const U32_NAME: &str = type_name::<u32>();
const U64_NAME: &str = type_name::<u64>();
const U128_NAME: &str = type_name::<u128>();

impl NumCast for Decimal {
    fn from<T: ToPrimitive>(n: T) -> Option<Self> {
        match type_name::<T>() {

            I8_NAME => return Decimal::from_i8(n.to_i8().unwrap()),
            I16_NAME => return Decimal::from_i16(n.to_i16().unwrap()),
            I64_NAME => return Decimal::from_i64(n.to_i64().unwrap()),
            I128_NAME => return Decimal::from_i128(n.to_i128().unwrap()),

            F32_NAME => return Decimal::from_f32(n.to_f32().unwrap()),
            F64_NAME => return Decimal::from_f64(n.to_f64().unwrap()),

            U8_NAME => return Decimal::from_u8(n.to_u8().unwrap()),
            U16_NAME => return Decimal::from_u16(n.to_u16().unwrap()),
            U32_NAME => return Decimal::from_u32(n.to_u32().unwrap()),
            U64_NAME => return Decimal::from_u64(n.to_u64().unwrap()),
            U128_NAME => return Decimal::from_u128(n.to_u128().unwrap()),
            _ => None
        }
    }
}

Another attempt using ID but has problems with static life time

use num_traits::cast::{FromPrimitive, NumCast, ToPrimitive};
use crate::prelude::Decimal;
use std::any::{Any, TypeId};
use alloc::boxed::Box;

impl<'a> NumCast for Decimal {
    fn from<T: ToPrimitive>(n: T) -> Option<Self> {

        let i8 = TypeId::of::<i8>();
        let i16 = TypeId::of::<i16>();
        let i64 = TypeId::of::<i64>();
        let i128 = TypeId::of::<i128>();

        let f32 = TypeId::of::<f32>();
        let f64 = TypeId::of::<f64>();

        let u8 = TypeId::of::<u8>();
        let u16 = TypeId::of::<u16>();
        let u32 = TypeId::of::<u32>();
        let u64 = TypeId::of::<u64>();
        let u128 = TypeId::of::<u128>();
        let boxed: Box<dyn Any> = Box::new(n);
        let actual_id = (&*boxed).type_id();

        match actual_id {
            i8 => return Decimal::from_i8(n.to_i8().unwrap()),
            i16 => return Decimal::from_i16(n.to_i16().unwrap()),
            i64 => return Decimal::from_i64(n.to_i64().unwrap()),
            i128 => return Decimal::from_i128(n.to_i128().unwrap()),

            f32 => return Decimal::from_f32(n.to_f32().unwrap()),
            f64 => return Decimal::from_f64(n.to_f64().unwrap()),

            u8 => return Decimal::from_u8(n.to_u8().unwrap()),
            u16 => return Decimal::from_u16(n.to_u16().unwrap()),
            u32 => return Decimal::from_u32(n.to_u32().unwrap()),
            u64 => return Decimal::from_u64(n.to_u64().unwrap()),
            u128 => return Decimal::from_u128(n.to_u128().unwrap()),
            _ => None
        }
    }
}

edit: fixed copy paste error where I used u8 everywhere edit: added second approach with issues around life times

paupino commented 1 year ago

Hmm, I wonder if you can achieve this by calling out to try_from (of which we implement for each primitive type) followed by ok() to turn it into an option? The one tricky part may be the ToPrimitive constraint - though perhaps you could guard against that too.

Something like:

impl NumCast for Decimal where Decimal: TryFrom<T>,
{
    fn from<T: ToPrimitive>(n: T) -> Option<Self> {
        Decimal::try_from(n).ok()
    }
}

The one thing that this implementation wouldn't do however is return None if the type was not implemented for Decimal - it'd be a compiler error instead I guess. In that case, you'd need to create a wrapper for Decimal and implement TryFrom/NumCast for the wrapper.

I'm a little nervous about doing explicit type dispatch - I feel that there is probably something we could do to get the compiler doing that for us instead, though I could be wrong.

I'm happy to take a look at this, perhaps later next week? That said: more than happy to review a PR if you get something working.

paupino commented 1 year ago

On second thoughts, this may be a bit trickier to implement because NumCast doesn't expose T at the trait level but instead at the function level... I'd need to have a deeper look into this.

MitchellNeill commented 1 year ago

It certainly makes it a better trickier, especially since we can't be sure if the input type implements any sort of helpful methods. At the moment I am working around it so no rush. Once I get a bit better at Rust lifetime I might take a look at the second option I listed.