How to decode and encode a float in Rust?
Asked Answered
A

4

5

I want to decode, store and encode a float in Rust. I know about num::Float::integer_decode() but I'd rather not lose any precision. That is, unless the format I encode into is smaller than the format I encode from of course.

Alkahest answered 13/10, 2016 at 20:39 Comment(2)
integer_decode() does not lose precision -- it's merely a deconstruction of the floating-point number into its components.Suckling
@trentcl I'm aware, but it appears encoding as in the documentation of the aforementioned function does.Alkahest
O
8

Newer versions of Rust provide safer options than some of the other answers suggest:

  • From Rust 1.20, you can use to_bits and from_bits to convert to and from the u64 binary representation.
  • From Rust 1.40, you can use to_be_bytes and from_be_bytes to deal with [u8; 8]. (There are also methods for little-endian byte order and native byte order.)
Oology answered 28/12, 2019 at 9:21 Comment(0)
P
3

Interpret the floating point bits as an integer and print out the value as hex:

use std::mem;

fn main() {
    let a_third: f64 = 1.0 / 3.0;

    let as_int: u64 = unsafe { mem::transmute(a_third) };
    println!("{}", as_int);

    let as_string = format!("{:016x}", as_int);
    println!("{}", as_string);

    let back_to_int = u64::from_str_radix(&as_string, 16).expect("Not an integer");
    println!("{}", back_to_int);

    let back_to_float: f64 = unsafe { mem::transmute(back_to_int) };
    println!("{}", back_to_float);

    assert_eq!(back_to_float, a_third);
}
Partlow answered 13/10, 2016 at 20:45 Comment(5)
Why not just transmute f64 to [u8; 8] and store it without unnecessary conversions?Lennox
@PavelStrakhov sounds like a prime candidate for an answer!Partlow
Oh! I was hoping for f32 and f64 to be directly encodable as hexadecimal (which is the easiest way to get a portable representation) but apparently they do not implement LowerHex or UpperHex. Disappointment :(Uturn
@MatthieuM. yeah, and there's no hex floating point literal either, so if you need to have a very specific float, you have to go through a transmute :-(Partlow
@Shepmaster: I think it would be worth raising a RFC here; both for support of literals and formatting/parsing. The formatting/parsing is much simpler than decimal format; literal parsing might require some tricks however (to avoid ambiguities with integrals).Uturn
L
2

If you don't intend to transfer serialized data between machines or you're certain that float representation is the same on all platforms you target, you can store byte representation of the number:

use std::io::{Read, Write};

fn main() {
  {
    let num: f64 = 1.0 / 3.0;
    let bytes: [u8; 8] = unsafe { std::mem::transmute(num) };
    let mut file = std::fs::File::create("/tmp/1").unwrap();
    file.write_all(&bytes).unwrap();
  }
  {
    let mut file = std::fs::File::open("/tmp/1").unwrap();
    let mut bytes: [u8; 8] = unsafe { std::mem::uninitialized() };
    file.read_exact(&mut bytes).unwrap();
    let num: f64 = unsafe { std::mem::transmute(bytes) };
    println!("result: {}", num);
  }
}

You can also use existing serialization framework, like serde. If you don't want the entire framework and just want to serialize floats, you can use dtoa (it's used by serde_json), although I'm not sure if it provides strong precision guarantees.

Lennox answered 14/10, 2016 at 12:6 Comment(0)
S
1

What's wrong with integer_decode()? It is lossless and works for finite numbers as well as NaN and infinities:

use std::mem;

fn integer_decode(val: f64) -> (u64, i16, i8) {
    let bits: u64 = unsafe { mem::transmute(val) };
    let sign: i8 = if bits >> 63 == 0 { 1 } else { -1 };
    let mut exponent: i16 = ((bits >> 52) & 0x7ff) as i16;
    let mantissa = if exponent == 0 {
        (bits & 0xfffffffffffff) << 1
    } else {
        (bits & 0xfffffffffffff) | 0x10000000000000
    };

    exponent -= 1023 + 52;
    (mantissa, exponent, sign)
}

fn main() {
    println!("{:?}", integer_decode(std::f64::NAN));
    println!("{:?}", integer_decode(std::f64::INFINITY));
    println!("{:?}", integer_decode(std::f64::NEG_INFINITY));
}
Schizophyceous answered 13/10, 2016 at 21:29 Comment(7)
Going from the docs that come with the function, it can lose precision during encoding.Alkahest
@JeroenBollen can you link these docs? I can't find this information in std. I implemented fn integer_encode((mantissa, exponent, sign): (u64, i16, i8)) -> f64 { (sign as f64) * (mantissa as f64) * (2f64.powf(exponent as f64)) } and it works for any X I feed into integer_encode(integer_decode(X)) except for NaN (maybe there is something wrong with my integer_encode, though).Schizophyceous
I did link to it in the OP.Alkahest
@JeroenBollen that link doesn't provide any information about loss of precision.Schizophyceous
The example provided only checks if the decoding is close to the original, not if it's exact. Decoding is done using multiplication which includes loss of precision.Alkahest
@JeroenBollen maybe that's only a case with f32? I can't reproduce any difference for f64, at least not enough to break assert_eq!(2.0, integer_encode(integer_decode(2.0))); like in the example in the docs you linked.Schizophyceous
Could just be a poorly written example. Multiplication shouldn't lose precision if the result is exactly representableSuckling

© 2022 - 2024 — McMap. All rights reserved.