aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe-php-src/src/main/snprintf.rs
blob: f1411d6e2ad8c071e4af133d433f3d28407de550 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
/// php-src: main/snprintf.c `php_gcvt` (PHP 8.5.8)
///
/// Only the `ndigit < 0` path (dtoa mode 0, the shortest round-trip representation used when
/// `serialize_precision=-1`) is ported; the fixed-precision mode 2 path is not needed yet.
/// The digit extraction delegates to Rust's own shortest round-trip float formatting, which
/// produces the same digit string as `zend_dtoa` in mode 0 (both compute the unique shortest
/// decimal that round-trips), so only the digit placement logic is ported here.
pub fn php_gcvt(value: f64, ndigit: i32, dec_point: char, exponent: char) -> String {
    let mode = if ndigit >= 0 { 2 } else { 0 };
    if mode != 0 {
        todo!("php_gcvt is only ported for ndigit < 0 (serialize_precision=-1)");
    }
    let ndigit = 17i32;

    let (sign, digits, decpt) = dtoa_shortest(value);

    let mut buf = String::new();
    if sign {
        buf.push('-');
    }

    if if decpt < 0 {
        decpt < -3
    } else {
        decpt > ndigit
    } {
        // exponential format (e.g. 1.0E+17)
        let exp = decpt - 1;
        let mut chars = digits.chars();
        buf.push(chars.next().expect("dtoa always yields at least one digit"));
        buf.push(dec_point);
        let rest = chars.as_str();
        if rest.is_empty() {
            buf.push('0');
        } else {
            buf.push_str(rest);
        }
        buf.push(exponent);
        if exp < 0 {
            buf.push('-');
        } else {
            buf.push('+');
        }
        buf.push_str(&exp.abs().to_string());
    } else if decpt > 0 {
        // standard format, integer part present
        let decpt = decpt as usize;
        if digits.len() <= decpt {
            buf.push_str(&digits);
            for _ in digits.len()..decpt {
                buf.push('0');
            }
        } else {
            buf.push_str(&digits[..decpt]);
            buf.push(dec_point);
            buf.push_str(&digits[decpt..]);
        }
    } else {
        // standard format, 0.000ddd
        buf.push('0');
        buf.push(dec_point);
        for _ in decpt..0 {
            buf.push('0');
        }
        buf.push_str(&digits);
    }

    buf
}

/// php-src: Zend/zend_strtod.c `zend_dtoa` mode 0 equivalent: the shortest round-trip digit
/// string of `|value|`, its sign, and the decimal point position (`value = 0.digits * 10^decpt`).
/// Implemented on top of Rust's `{:e}` formatting, which is also shortest-round-trip.
fn dtoa_shortest(value: f64) -> (bool, String, i32) {
    let sign = value.is_sign_negative();
    let formatted = format!("{:e}", value.abs());
    let (mantissa, exp) = formatted
        .split_once('e')
        .expect("`{:e}` always contains an exponent");
    let digits: String = mantissa.chars().filter(|c| *c != '.').collect();
    let exp: i32 = exp.parse().expect("`{:e}` exponent is a decimal integer");
    (sign, digits, exp + 1)
}