Add support for use num_traits::cast::NumCast
MitchellNeill opened this issue · 3 comments
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:
- Figure out the input type and call the appropriate conversion method (requires figuring out input type)
- Always convert input to an f64 and convert f64 to a Decimal (has some data loss when input is i128)
- 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
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.
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.
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.