Skip to main content

exiftool_rs/
value.rs

1use std::fmt;
2
3/// Represents a metadata tag value, which can be of various types.
4#[derive(Debug, Clone, PartialEq)]
5pub enum Value {
6    /// ASCII/UTF-8 string
7    String(String),
8    /// Unsigned 8-bit integer
9    U8(u8),
10    /// Unsigned 16-bit integer
11    U16(u16),
12    /// Unsigned 32-bit integer
13    U32(u32),
14    /// Signed 16-bit integer
15    I16(i16),
16    /// Signed 32-bit integer
17    I32(i32),
18    /// Unsigned rational (numerator/denominator)
19    URational(u32, u32),
20    /// Signed rational (numerator/denominator)
21    IRational(i32, i32),
22    /// 32-bit float
23    F32(f32),
24    /// 64-bit float
25    F64(f64),
26    /// Raw binary data
27    Binary(Vec<u8>),
28    /// A list of values (e.g., GPS coordinates, color space arrays)
29    List(Vec<Value>),
30    /// Undefined/opaque bytes with a semantic type hint
31    Undefined(Vec<u8>),
32}
33
34impl Value {
35    /// Convert to string representation (PrintConv equivalent).
36    pub fn to_display_string(&self) -> String {
37        match self {
38            Value::String(s) => s.clone(),
39            Value::U8(v) => v.to_string(),
40            Value::U16(v) => v.to_string(),
41            Value::U32(v) => v.to_string(),
42            Value::I16(v) => v.to_string(),
43            Value::I32(v) => v.to_string(),
44            Value::URational(n, d) => {
45                if *d == 0 {
46                    if *n == 0 {
47                        "undef".to_string()
48                    } else {
49                        "inf".to_string()
50                    }
51                } else if *n % *d == 0 {
52                    (*n / *d).to_string()
53                } else {
54                    format!("{}/{}", n, d)
55                }
56            }
57            Value::IRational(n, d) => {
58                if *d == 0 {
59                    if *n >= 0 {
60                        "inf".to_string()
61                    } else {
62                        "-inf".to_string()
63                    }
64                } else if *n % *d == 0 {
65                    (*n / *d).to_string()
66                } else {
67                    format!("{}/{}", n, d)
68                }
69            }
70            Value::F32(v) => format!("{}", v),
71            Value::F64(v) => format!("{}", v),
72            Value::Binary(data) => format!("(Binary data {} bytes)", data.len()),
73            Value::List(items) => items
74                .iter()
75                .map(|v| v.to_display_string())
76                .collect::<Vec<_>>()
77                .join(", "),
78            Value::Undefined(data) => format!("(Undefined {} bytes)", data.len()),
79        }
80    }
81
82    /// Try to interpret the value as a float.
83    pub fn as_f64(&self) -> Option<f64> {
84        match self {
85            Value::U8(v) => Some(*v as f64),
86            Value::U16(v) => Some(*v as f64),
87            Value::U32(v) => Some(*v as f64),
88            Value::I16(v) => Some(*v as f64),
89            Value::I32(v) => Some(*v as f64),
90            Value::F32(v) => Some(*v as f64),
91            Value::F64(v) => Some(*v),
92            Value::URational(n, d) if *d != 0 => Some(*n as f64 / *d as f64),
93            Value::IRational(n, d) if *d != 0 => Some(*n as f64 / *d as f64),
94            _ => None,
95        }
96    }
97
98    /// Try to interpret the value as a string.
99    pub fn as_str(&self) -> Option<&str> {
100        match self {
101            Value::String(s) => Some(s),
102            _ => None,
103        }
104    }
105
106    /// Try to interpret the value as an unsigned integer.
107    pub fn as_u64(&self) -> Option<u64> {
108        match self {
109            Value::U8(v) => Some(*v as u64),
110            Value::U16(v) => Some(*v as u64),
111            Value::U32(v) => Some(*v as u64),
112            _ => None,
113        }
114    }
115}
116
117impl fmt::Display for Value {
118    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
119        write!(f, "{}", self.to_display_string())
120    }
121}
122
123/// Format a float with Perl-style %.15g precision (15 significant digits, trailing zeros stripped).
124/// This matches ExifTool's default `%s` formatting for floating-point values.
125pub fn format_g15(v: f64) -> String {
126    format_g_prec(v, 15)
127}
128
129/// Format a float with Perl-style %.Ng precision (N significant digits, trailing zeros stripped).
130/// Mirrors C sprintf's %g: uses exponential if exponent < -4 or >= precision.
131pub fn format_g_prec(v: f64, prec: usize) -> String {
132    if v == 0.0 {
133        return "0".to_string();
134    }
135    let abs_v = v.abs();
136    let exp = abs_v.log10().floor() as i32;
137    if exp >= -4 && exp < prec as i32 {
138        // Fixed-point: need (prec-1 - exp) decimal places
139        let decimal_places = ((prec as i32 - 1 - exp).max(0)) as usize;
140        let s = format!("{:.prec$}", v, prec = decimal_places);
141        if s.contains('.') {
142            s.trim_end_matches('0').trim_end_matches('.').to_string()
143        } else {
144            s
145        }
146    } else {
147        // Exponential format: prec-1 decimal places
148        let decimal_places = prec - 1;
149        let s = format!("{:.prec$e}", v, prec = decimal_places);
150        // Rust produces e.g. "3.51360899930879e20", need "3.51360899930879e+20"
151        // and "-1.5e-6" → "-1.5e-06" (at least 2 digits in exponent)
152        // First strip trailing zeros from mantissa
153        let (mantissa_part, exp_part) = if let Some(e_pos) = s.find('e') {
154            (&s[..e_pos], &s[e_pos + 1..])
155        } else {
156            return s;
157        };
158        let mantissa_trimmed = if mantissa_part.contains('.') {
159            mantissa_part.trim_end_matches('0').trim_end_matches('.')
160        } else {
161            mantissa_part
162        };
163        // Parse exponent and reformat with sign and minimum 2 digits
164        let exp_val: i32 = exp_part.parse().unwrap_or(0);
165        let exp_str = if exp_val >= 0 {
166            format!("e+{:02}", exp_val)
167        } else {
168            format!("e-{:02}", -exp_val)
169        };
170        format!("{}{}", mantissa_trimmed, exp_str)
171    }
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177
178    // ── to_display_string ──────────────────────────────────────────
179
180    #[test]
181    fn display_string() {
182        assert_eq!(Value::String("hello".into()).to_display_string(), "hello");
183    }
184
185    #[test]
186    fn display_u8() {
187        assert_eq!(Value::U8(42).to_display_string(), "42");
188    }
189
190    #[test]
191    fn display_u16() {
192        assert_eq!(Value::U16(1024).to_display_string(), "1024");
193    }
194
195    #[test]
196    fn display_u32() {
197        assert_eq!(Value::U32(100_000).to_display_string(), "100000");
198    }
199
200    #[test]
201    fn display_i16() {
202        assert_eq!(Value::I16(-123).to_display_string(), "-123");
203    }
204
205    #[test]
206    fn display_i32() {
207        assert_eq!(Value::I32(-999_999).to_display_string(), "-999999");
208    }
209
210    #[test]
211    fn display_urational_exact_division() {
212        assert_eq!(Value::URational(100, 10).to_display_string(), "10");
213    }
214
215    #[test]
216    fn display_urational_non_exact() {
217        assert_eq!(Value::URational(1, 3).to_display_string(), "1/3");
218    }
219
220    #[test]
221    fn display_urational_zero_zero() {
222        assert_eq!(Value::URational(0, 0).to_display_string(), "undef");
223    }
224
225    #[test]
226    fn display_urational_n_over_zero() {
227        assert_eq!(Value::URational(5, 0).to_display_string(), "inf");
228    }
229
230    #[test]
231    fn display_irational_exact() {
232        assert_eq!(Value::IRational(-10, 5).to_display_string(), "-2");
233    }
234
235    #[test]
236    fn display_irational_non_exact() {
237        assert_eq!(Value::IRational(7, 3).to_display_string(), "7/3");
238    }
239
240    #[test]
241    fn display_irational_positive_inf() {
242        assert_eq!(Value::IRational(1, 0).to_display_string(), "inf");
243    }
244
245    #[test]
246    fn display_irational_zero_inf() {
247        // n=0 d=0 → n >= 0, so "inf"
248        assert_eq!(Value::IRational(0, 0).to_display_string(), "inf");
249    }
250
251    #[test]
252    fn display_irational_negative_inf() {
253        assert_eq!(Value::IRational(-3, 0).to_display_string(), "-inf");
254    }
255
256    #[test]
257    fn display_f32() {
258        let s = Value::F32(3.14).to_display_string();
259        assert!(s.starts_with("3.14"), "got: {}", s);
260    }
261
262    #[test]
263    fn display_f64() {
264        assert_eq!(Value::F64(2.5).to_display_string(), "2.5");
265    }
266
267    #[test]
268    fn display_binary() {
269        assert_eq!(
270            Value::Binary(vec![0, 1, 2]).to_display_string(),
271            "(Binary data 3 bytes)"
272        );
273    }
274
275    #[test]
276    fn display_list() {
277        let list = Value::List(vec![Value::U16(640), Value::U16(480)]);
278        assert_eq!(list.to_display_string(), "640, 480");
279    }
280
281    #[test]
282    fn display_undefined() {
283        assert_eq!(
284            Value::Undefined(vec![0xAB; 5]).to_display_string(),
285            "(Undefined 5 bytes)"
286        );
287    }
288
289    // ── as_f64 ─────────────────────────────────────────────────────
290
291    #[test]
292    fn as_f64_u8() {
293        assert_eq!(Value::U8(10).as_f64(), Some(10.0));
294    }
295
296    #[test]
297    fn as_f64_u16() {
298        assert_eq!(Value::U16(300).as_f64(), Some(300.0));
299    }
300
301    #[test]
302    fn as_f64_u32() {
303        assert_eq!(Value::U32(70_000).as_f64(), Some(70_000.0));
304    }
305
306    #[test]
307    fn as_f64_i16() {
308        assert_eq!(Value::I16(-50).as_f64(), Some(-50.0));
309    }
310
311    #[test]
312    fn as_f64_i32() {
313        assert_eq!(Value::I32(-1_000_000).as_f64(), Some(-1_000_000.0));
314    }
315
316    #[test]
317    fn as_f64_f32() {
318        let val = Value::F32(1.5).as_f64().unwrap();
319        assert!((val - 1.5).abs() < 1e-6);
320    }
321
322    #[test]
323    fn as_f64_f64() {
324        assert_eq!(Value::F64(9.99).as_f64(), Some(9.99));
325    }
326
327    #[test]
328    fn as_f64_urational() {
329        let val = Value::URational(1, 4).as_f64().unwrap();
330        assert!((val - 0.25).abs() < 1e-10);
331    }
332
333    #[test]
334    fn as_f64_urational_zero_denom() {
335        assert_eq!(Value::URational(5, 0).as_f64(), None);
336    }
337
338    #[test]
339    fn as_f64_irational() {
340        let val = Value::IRational(-3, 2).as_f64().unwrap();
341        assert!((val - -1.5).abs() < 1e-10);
342    }
343
344    #[test]
345    fn as_f64_irational_zero_denom() {
346        assert_eq!(Value::IRational(-1, 0).as_f64(), None);
347    }
348
349    #[test]
350    fn as_f64_string_none() {
351        assert_eq!(Value::String("hi".into()).as_f64(), None);
352    }
353
354    #[test]
355    fn as_f64_binary_none() {
356        assert_eq!(Value::Binary(vec![1]).as_f64(), None);
357    }
358
359    #[test]
360    fn as_f64_undefined_none() {
361        assert_eq!(Value::Undefined(vec![1]).as_f64(), None);
362    }
363
364    // ── as_str ─────────────────────────────────────────────────────
365
366    #[test]
367    fn as_str_string() {
368        assert_eq!(Value::String("test".into()).as_str(), Some("test"));
369    }
370
371    #[test]
372    fn as_str_non_string() {
373        assert_eq!(Value::U8(1).as_str(), None);
374        assert_eq!(Value::Binary(vec![]).as_str(), None);
375        assert_eq!(Value::F64(1.0).as_str(), None);
376    }
377
378    // ── as_u64 ─────────────────────────────────────────────────────
379
380    #[test]
381    fn as_u64_unsigned_types() {
382        assert_eq!(Value::U8(255).as_u64(), Some(255));
383        assert_eq!(Value::U16(65535).as_u64(), Some(65535));
384        assert_eq!(Value::U32(0xFFFFFFFF).as_u64(), Some(0xFFFFFFFF));
385    }
386
387    #[test]
388    fn as_u64_signed_none() {
389        assert_eq!(Value::I16(1).as_u64(), None);
390        assert_eq!(Value::I32(1).as_u64(), None);
391    }
392
393    #[test]
394    fn as_u64_other_none() {
395        assert_eq!(Value::String("42".into()).as_u64(), None);
396        assert_eq!(Value::F64(1.0).as_u64(), None);
397        assert_eq!(Value::Binary(vec![]).as_u64(), None);
398        assert_eq!(Value::Undefined(vec![]).as_u64(), None);
399    }
400
401    // ── Display trait ──────────────────────────────────────────────
402
403    #[test]
404    fn display_trait_delegates() {
405        let v = Value::URational(1, 3);
406        assert_eq!(format!("{}", v), "1/3");
407    }
408
409    // ── format_g15 / format_g_prec ─────────────────────────────────
410
411    #[test]
412    fn format_g15_zero() {
413        assert_eq!(format_g15(0.0), "0");
414    }
415
416    #[test]
417    fn format_g15_integer() {
418        assert_eq!(format_g15(42.0), "42");
419    }
420
421    #[test]
422    fn format_g15_simple_decimal() {
423        assert_eq!(format_g15(3.5), "3.5");
424    }
425
426    #[test]
427    fn format_g15_negative() {
428        assert_eq!(format_g15(-1.25), "-1.25");
429    }
430
431    #[test]
432    fn format_g15_large_value_scientific() {
433        // 1e+20 should use exponential
434        let s = format_g15(1e20);
435        assert!(s.contains("e+"), "expected scientific notation, got: {}", s);
436    }
437
438    #[test]
439    fn format_g15_small_value_scientific() {
440        // 1e-5 is < 1e-4, should use exponential
441        let s = format_g15(1e-5);
442        assert!(s.contains("e-"), "expected scientific notation, got: {}", s);
443    }
444
445    #[test]
446    fn format_g15_borderline_fixed() {
447        // 0.0001 = 1e-4 → exp = -4, which is >= -4, so fixed format
448        let s = format_g15(0.0001);
449        assert_eq!(s, "0.0001");
450    }
451
452    #[test]
453    fn format_g_prec_low_precision() {
454        // 3 significant digits for pi
455        let s = format_g_prec(std::f64::consts::PI, 3);
456        assert_eq!(s, "3.14");
457    }
458
459    #[test]
460    fn format_g_prec_one_digit() {
461        let s = format_g_prec(7.7, 1);
462        assert_eq!(s, "8");
463    }
464
465    #[test]
466    fn format_g15_trailing_zeros_stripped() {
467        // 1.5 should not produce "1.500000..."
468        let s = format_g15(1.5);
469        assert!(!s.ends_with('0'), "trailing zeros not stripped: {}", s);
470    }
471
472    #[test]
473    fn format_g15_very_large() {
474        let s = format_g15(1.23456789e+100);
475        assert!(s.starts_with("1.23456789"), "got: {}", s);
476        assert!(s.contains("e+100"), "got: {}", s);
477    }
478
479    #[test]
480    fn format_g15_very_small_negative() {
481        let s = format_g15(-5.5e-10);
482        assert!(s.starts_with("-5.5e-"), "got: {}", s);
483    }
484}