|
| 1 | +#[cfg(feature = "approx")] |
| 2 | +use {approx::assert_abs_diff_eq, ndarray::prelude::*, std::convert::TryFrom}; |
| 3 | + |
| 4 | +#[cfg(feature = "approx")] |
| 5 | +fn main() { |
| 6 | + // Converting an array from one datatype to another is implemented with the |
| 7 | + // `ArrayBase::mapv()` function. We pass a closure that is applied to each |
| 8 | + // element independently. This allows for more control and flexiblity in |
| 9 | + // converting types. |
| 10 | + // |
| 11 | + // Below, we illustrate four different approaches for the actual conversion |
| 12 | + // in the closure. |
| 13 | + // - `From` ensures lossless conversions known at compile time and is the |
| 14 | + // best default choice. |
| 15 | + // - `TryFrom` either converts data losslessly or panics, ensuring that the |
| 16 | + // rest of the program does not continue with unexpected data. |
| 17 | + // - `as` never panics and may silently convert in a lossy way, depending |
| 18 | + // on the source and target datatypes. More details can be found in the |
| 19 | + // reference: https://doc.rust-lang.org/reference/expressions/operator-expr.html#numeric-cast |
| 20 | + // - Using custom logic in the closure, e.g. to clip values or for NaN |
| 21 | + // handling in floats. |
| 22 | + // |
| 23 | + // For a brush-up on casting between numeric types in Rust, refer to: |
| 24 | + // https://doc.rust-lang.org/rust-by-example/types/cast.html |
| 25 | + |
| 26 | + // Infallible, lossless conversion with `From` |
| 27 | + // The trait `std::convert::From` is only implemented for conversions that |
| 28 | + // can be guaranteed to be lossless at compile time. This is the safest |
| 29 | + // approach. |
| 30 | + let a_u8: Array<u8, _> = array![[1, 2, 3], [4, 5, 6]]; |
| 31 | + let a_f32 = a_u8.mapv(|element| f32::from(element)); |
| 32 | + assert_abs_diff_eq!(a_f32, array![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]); |
| 33 | + |
| 34 | + // Fallible, lossless conversion with `TryFrom` |
| 35 | + // `i8` numbers can be negative, in such a case, there is no perfect |
| 36 | + // conversion to `u8` defined. In this example, all numbers are positive and |
| 37 | + // in bounds and can be converted at runtime. But for unknown runtime input, |
| 38 | + // this would panic with the message provided in `.expect()`. Note that you |
| 39 | + // can also use `.unwrap()` to be more concise. |
| 40 | + let a_i8: Array<i8, _> = array![120, 8, 0]; |
| 41 | + let a_u8 = a_i8.mapv(|element| u8::try_from(element).expect("Could not convert i8 to u8")); |
| 42 | + assert_eq!(a_u8, array![120u8, 8u8, 0u8]); |
| 43 | + |
| 44 | + // Unsigned to signed integer conversion with `as` |
| 45 | + // A real-life example of this would be coordinates on a grid. |
| 46 | + // A `usize` value can be larger than what fits into a `isize`, therefore, |
| 47 | + // it would be safer to use `TryFrom`. Nevertheless, `as` can be used for |
| 48 | + // either simplicity or performance. |
| 49 | + // The example includes `usize::MAX` to illustrate potentially undesired |
| 50 | + // behavior. It will be interpreted as -1 (noop-casting + 2-complement), see |
| 51 | + // https://doc.rust-lang.org/reference/expressions/operator-expr.html#numeric-cast |
| 52 | + let a_usize: Array<usize, _> = array![1, 2, 3, usize::MAX]; |
| 53 | + let a_isize = a_usize.mapv(|element| element as isize); |
| 54 | + assert_eq!(a_isize, array![1_isize, 2_isize, 3_isize, -1_isize]); |
| 55 | + |
| 56 | + // Simple upcasting with `as` |
| 57 | + // Every `u8` fits perfectly into a `u32`, therefore this is a lossless |
| 58 | + // conversion. |
| 59 | + // Note that it is up to the programmer to ensure the validity of the |
| 60 | + // conversion over the lifetime of a program. With type inference, subtle |
| 61 | + // bugs can creep in since conversions with `as` will always compile, so a |
| 62 | + // programmer might not notice that a prior lossless conversion became a |
| 63 | + // lossy conversion. With `From`, this would be noticed at compile-time and |
| 64 | + // with `TryFrom`, it would also be either handled or make the program |
| 65 | + // panic. |
| 66 | + let a_u8: Array<u8, _> = array![[1, 2, 3], [4, 5, 6]]; |
| 67 | + let a_u32 = a_u8.mapv(|element| element as u32); |
| 68 | + assert_eq!(a_u32, array![[1u32, 2u32, 3u32], [4u32, 5u32, 6u32]]); |
| 69 | + |
| 70 | + // Saturating cast with `as` |
| 71 | + // The `as` keyword performs a *saturating cast* When casting floats to |
| 72 | + // ints. This means that numbers which do not fit into the target datatype |
| 73 | + // will silently be clipped to the maximum/minimum numbers. Since this is |
| 74 | + // not obvious, we discourage the intentional use of casting with `as` with |
| 75 | + // silent saturation and recommend a custom logic instead which makes the |
| 76 | + // intent clear. |
| 77 | + let a_f32: Array<f32, _> = array![ |
| 78 | + 256.0, // saturated to 255 |
| 79 | + 255.7, // saturated to 255 |
| 80 | + 255.1, // saturated to 255 |
| 81 | + 254.7, // rounded down to 254 by cutting the decimal part |
| 82 | + 254.1, // rounded down to 254 by cutting the decimal part |
| 83 | + -1.0, // saturated to 0 on the lower end |
| 84 | + f32::INFINITY, // saturated to 255 |
| 85 | + f32::NAN, // converted to zero |
| 86 | + ]; |
| 87 | + let a_u8 = a_f32.mapv(|element| element as u8); |
| 88 | + assert_eq!(a_u8, array![255, 255, 255, 254, 254, 0, 255, 0]); |
| 89 | + |
| 90 | + // Custom mapping logic |
| 91 | + // Given that we pass a closure for the conversion, we can also define |
| 92 | + // custom logic to e.g. replace NaN values and clip others. This also |
| 93 | + // makes the intent clear. |
| 94 | + let a_f32: Array<f32, _> = array![ |
| 95 | + 270.0, // clipped to 200 |
| 96 | + -1.2, // clipped to 0 |
| 97 | + 4.7, // rounded up to 5 instead of just stripping decimals |
| 98 | + f32::INFINITY, // clipped to 200 |
| 99 | + f32::NAN, // replaced with upper bound 200 |
| 100 | + ]; |
| 101 | + let a_u8_custom = a_f32.mapv(|element| { |
| 102 | + if element == f32::INFINITY || element.is_nan() { |
| 103 | + return 200; |
| 104 | + } |
| 105 | + if let Some(std::cmp::Ordering::Less) = element.partial_cmp(&0.0) { |
| 106 | + return 0; |
| 107 | + } |
| 108 | + 200.min(element.round() as u8) |
| 109 | + }); |
| 110 | + assert_eq!(a_u8_custom, array![200, 0, 5, 200, 200]); |
| 111 | +} |
| 112 | + |
| 113 | +#[cfg(not(feature = "approx"))] |
| 114 | +fn main() {} |
0 commit comments