1
+ /// Format helper functions for sprintf-style operations
2
+ ///
3
+ /// This module provides utilities for handling Perl sprintf patterns,
4
+ /// particularly those involving split operations that produce variable
5
+ /// numbers of arguments.
6
+
7
+ pub mod sprintf;
8
+ pub mod unpack;
9
+
10
+ pub use sprintf:: sprintf_perl;
11
+ pub use unpack:: unpack_binary;
12
+
13
+ use crate :: types:: TagValue ;
14
+
15
+ /// Format values from a split operation using a sprintf-style format string
16
+ ///
17
+ /// This handles cases like `sprintf("%.3f x %.3f mm", split(" ",$val))`
18
+ /// where split produces a variable number of values that need to be formatted.
19
+ pub fn sprintf_split_values ( format_str : & str , values : & [ TagValue ] ) -> String {
20
+ // Count the number of format specifiers in the format string
21
+ let format_specs = format_str. matches ( "%.3f" ) . count ( )
22
+ + format_str. matches ( "%.2f" ) . count ( )
23
+ + format_str. matches ( "%.1f" ) . count ( )
24
+ + format_str. matches ( "%d" ) . count ( )
25
+ + format_str. matches ( "%s" ) . count ( )
26
+ + format_str. matches ( "%f" ) . count ( ) ;
27
+
28
+ // Convert values to strings based on the format specifiers
29
+ let formatted_values: Vec < String > = values. iter ( )
30
+ . take ( format_specs)
31
+ . map ( |v| match v {
32
+ TagValue :: F64 ( f) => {
33
+ // Check what precision is needed based on format string
34
+ if format_str. contains ( "%.3f" ) {
35
+ format ! ( "{:.3}" , f)
36
+ } else if format_str. contains ( "%.2f" ) {
37
+ format ! ( "{:.2}" , f)
38
+ } else if format_str. contains ( "%.1f" ) {
39
+ format ! ( "{:.1}" , f)
40
+ } else {
41
+ f. to_string ( )
42
+ }
43
+ }
44
+ TagValue :: I32 ( i) => i. to_string ( ) ,
45
+ TagValue :: String ( s) => s. clone ( ) ,
46
+ _ => v. to_string ( ) ,
47
+ } )
48
+ . collect ( ) ;
49
+
50
+ // Build the final formatted string
51
+ match formatted_values. len ( ) {
52
+ 0 => String :: new ( ) ,
53
+ 1 if format_specs == 2 => {
54
+ // Special case: one value but format expects two (like "%.3f x %.3f mm")
55
+ // Use the same value twice
56
+ apply_format ( format_str, & [ formatted_values[ 0 ] . clone ( ) , formatted_values[ 0 ] . clone ( ) ] )
57
+ }
58
+ _ => apply_format ( format_str, & formatted_values)
59
+ }
60
+ }
61
+
62
+ /// Apply a format string with the given values
63
+ fn apply_format ( format_str : & str , values : & [ String ] ) -> String {
64
+ let mut result = format_str. to_string ( ) ;
65
+ let mut value_iter = values. iter ( ) ;
66
+
67
+ // Replace format specifiers in order
68
+ let patterns = [ "%.3f" , "%.2f" , "%.1f" , "%d" , "%s" , "%f" ] ;
69
+
70
+ for pattern in & patterns {
71
+ while result. contains ( pattern) {
72
+ if let Some ( value) = value_iter. next ( ) {
73
+ result = result. replacen ( pattern, value, 1 ) ;
74
+ } else {
75
+ // No more values, break
76
+ break ;
77
+ }
78
+ }
79
+ }
80
+
81
+ result
82
+ }
83
+
84
+ /// Handle sprintf with Perl string concatenation and repetition operations
85
+ ///
86
+ /// This handles cases like `sprintf("%19d %4d %6d" . " %3d %4d %6d" x 8, split(" ",$val))`
87
+ /// where the format string is built from concatenation and repetition before sprintf.
88
+ ///
89
+ /// # Arguments
90
+ /// * `base_format` - The base format string (e.g., "%19d %4d %6d")
91
+ /// * `concat_part` - The part to concatenate and repeat (e.g., " %3d %4d %6d")
92
+ /// * `repeat_count` - How many times to repeat the concat_part (e.g., 8)
93
+ /// * `args` - The arguments to format with the completed format string (can be a single TagValue or array)
94
+ ///
95
+ /// # Example
96
+ /// ```
97
+ /// # use exif_oxide::fmt::sprintf_with_string_concat_repeat;
98
+ /// # use exif_oxide::types::TagValue;
99
+ /// let result = sprintf_with_string_concat_repeat(
100
+ /// "%19d %4d %6d",
101
+ /// " %3d %4d %6d",
102
+ /// 8,
103
+ /// &TagValue::Array(vec![TagValue::I32(1), TagValue::I32(2), TagValue::I32(3)])
104
+ /// );
105
+ /// // Builds format: "%19d %4d %6d %3d %4d %6d %3d %4d %6d ..." (repeated 8 times)
106
+ /// ```
107
+ pub fn sprintf_with_string_concat_repeat (
108
+ base_format : & str ,
109
+ concat_part : & str ,
110
+ repeat_count : usize ,
111
+ args : & TagValue
112
+ ) -> String {
113
+ // Build complete format string: base + (concat_part repeated repeat_count times)
114
+ let complete_format = format ! ( "{}{}" , base_format, concat_part. repeat( repeat_count) ) ;
115
+
116
+ // Convert TagValue to slice for sprintf_perl
117
+ match args {
118
+ TagValue :: Array ( arr) => sprintf_perl ( & complete_format, arr) ,
119
+ single_val => sprintf_perl ( & complete_format, & [ single_val. clone ( ) ] ) ,
120
+ }
121
+ }
122
+
123
+ /// Safe division calculation following ExifTool pattern: $val ? numerator / $val : 0
124
+ ///
125
+ /// This implements the common ExifTool pattern of computing a safe division
126
+ /// while handling zero, empty, or undefined values by returning 0 instead of
127
+ /// infinity or division-by-zero errors.
128
+ ///
129
+ /// # Arguments
130
+ /// * `numerator` - The numerator for the division (e.g., 1.0, 10.0)
131
+ /// * `val` - The TagValue to use as denominator
132
+ ///
133
+ /// # Returns
134
+ /// * If val is truthy and non-zero: TagValue::F64(numerator / val)
135
+ /// * If val is falsy or zero: TagValue::F64(0.0)
136
+ pub fn safe_division ( numerator : f64 , val : & TagValue ) -> TagValue {
137
+ // Check if value is truthy (non-zero, non-empty) following ExifTool semantics
138
+ let is_truthy = match val {
139
+ TagValue :: U8 ( v) => * v != 0 ,
140
+ TagValue :: U16 ( v) => * v != 0 ,
141
+ TagValue :: U32 ( v) => * v != 0 ,
142
+ TagValue :: U64 ( v) => * v != 0 ,
143
+ TagValue :: I16 ( v) => * v != 0 ,
144
+ TagValue :: I32 ( v) => * v != 0 ,
145
+ TagValue :: F64 ( v) => * v != 0.0 && !v. is_nan ( ) ,
146
+ TagValue :: String ( s) => !s. is_empty ( ) && s != "0" ,
147
+ TagValue :: Rational ( num, _) => * num != 0 ,
148
+ TagValue :: SRational ( num, _) => * num != 0 ,
149
+ TagValue :: Empty => false ,
150
+ _ => true , // Arrays, objects, etc. are considered truthy if they exist
151
+ } ;
152
+
153
+ if !is_truthy {
154
+ return TagValue :: F64 ( 0.0 ) ;
155
+ }
156
+
157
+ // Extract numeric value and compute division
158
+ match val {
159
+ TagValue :: U8 ( v) => TagValue :: F64 ( numerator / ( * v as f64 ) ) ,
160
+ TagValue :: U16 ( v) => TagValue :: F64 ( numerator / ( * v as f64 ) ) ,
161
+ TagValue :: U32 ( v) => TagValue :: F64 ( numerator / ( * v as f64 ) ) ,
162
+ TagValue :: U64 ( v) => TagValue :: F64 ( numerator / ( * v as f64 ) ) ,
163
+ TagValue :: I16 ( v) => TagValue :: F64 ( numerator / ( * v as f64 ) ) ,
164
+ TagValue :: I32 ( v) => TagValue :: F64 ( numerator / ( * v as f64 ) ) ,
165
+ TagValue :: F64 ( v) => TagValue :: F64 ( numerator / v) ,
166
+ TagValue :: String ( s) => {
167
+ if let Ok ( num) = s. parse :: < f64 > ( ) {
168
+ if num != 0.0 {
169
+ TagValue :: F64 ( numerator / num)
170
+ } else {
171
+ TagValue :: F64 ( 0.0 )
172
+ }
173
+ } else {
174
+ // Non-numeric string - return 0 following ExifTool semantics
175
+ TagValue :: F64 ( 0.0 )
176
+ }
177
+ }
178
+ TagValue :: Rational ( num, denom) => {
179
+ if * num != 0 && * denom != 0 {
180
+ let val = * num as f64 / * denom as f64 ;
181
+ TagValue :: F64 ( numerator / val)
182
+ } else {
183
+ TagValue :: F64 ( 0.0 )
184
+ }
185
+ }
186
+ TagValue :: SRational ( num, denom) => {
187
+ if * num != 0 && * denom != 0 {
188
+ let val = * num as f64 / * denom as f64 ;
189
+ TagValue :: F64 ( numerator / val)
190
+ } else {
191
+ TagValue :: F64 ( 0.0 )
192
+ }
193
+ }
194
+ _ => TagValue :: F64 ( 0.0 ) , // For complex types, return 0
195
+ }
196
+ }
197
+
198
+ /// Safe reciprocal calculation following ExifTool pattern: $val ? 1 / $val : 0
199
+ ///
200
+ /// This is a convenience wrapper around `safe_division(1.0, val)`.
201
+ ///
202
+ /// Common use cases:
203
+ /// - Converting focal length to optical power (diopters)
204
+ /// - Converting f-number to relative aperture calculations
205
+ /// - Any reciprocal calculation where 0 input should yield 0 output
206
+ ///
207
+ /// # Arguments
208
+ /// * `val` - The TagValue to take the reciprocal of
209
+ ///
210
+ /// # Returns
211
+ /// * If val is truthy and non-zero: TagValue::F64(1.0 / val)
212
+ /// * If val is falsy or zero: TagValue::F64(0.0)
213
+ ///
214
+ /// # Example
215
+ /// ```rust
216
+ /// # use exif_oxide::fmt::safe_reciprocal;
217
+ /// # use exif_oxide::types::TagValue;
218
+ ///
219
+ /// // Normal case: 1/2 = 0.5
220
+ /// assert_eq!(safe_reciprocal(&TagValue::I32(2)), TagValue::F64(0.5));
221
+ ///
222
+ /// // Zero case: avoid division by zero
223
+ /// assert_eq!(safe_reciprocal(&TagValue::I32(0)), TagValue::F64(0.0));
224
+ ///
225
+ /// // Empty string case: treat as zero
226
+ /// assert_eq!(safe_reciprocal(&TagValue::String("".to_string())), TagValue::F64(0.0));
227
+ /// ```
228
+ pub fn safe_reciprocal ( val : & TagValue ) -> TagValue {
229
+ safe_division ( 1.0 , val)
230
+ }
231
+
232
+ /// Pack "C*" with bit extraction pattern
233
+ ///
234
+ /// Implements: pack "C*", map { (($val>>$_)&mask)+offset } shifts...
235
+ /// This extracts specific bit ranges from val at different shift positions,
236
+ /// applies a mask and offset, then packs as unsigned chars into a binary string.
237
+ ///
238
+ /// # Arguments
239
+ /// * `val` - The TagValue to extract bits from
240
+ /// * `shifts` - Array of bit shift positions
241
+ /// * `mask` - Bitmask to apply after shifting
242
+ /// * `offset` - Offset to add to each extracted value
243
+ ///
244
+ /// # Example
245
+ /// ```rust
246
+ /// # use exif_oxide::fmt::pack_c_star_bit_extract;
247
+ /// # use exif_oxide::types::TagValue;
248
+ /// // Equivalent to: pack "C*", map { (($val>>$_)&0x1f)+0x60 } 10, 5, 0
249
+ /// let result = pack_c_star_bit_extract(&TagValue::I32(0x1234), &[10, 5, 0], 0x1f, 0x60);
250
+ /// ```
251
+ pub fn pack_c_star_bit_extract ( val : & TagValue , shifts : & [ i32 ] , mask : i32 , offset : i32 ) -> TagValue {
252
+ // Extract numeric value from TagValue
253
+ let numeric_val = match val {
254
+ TagValue :: I32 ( i) => * i,
255
+ TagValue :: F64 ( f) => * f as i32 ,
256
+ TagValue :: String ( s) => s. parse :: < i32 > ( ) . unwrap_or ( 0 ) ,
257
+ _ => 0 ,
258
+ } ;
259
+
260
+ let bytes: Vec < u8 > = shifts. iter ( )
261
+ . map ( |& shift| ( ( ( numeric_val >> shift) & mask) + offset) as u8 )
262
+ . collect ( ) ;
263
+
264
+ TagValue :: String ( String :: from_utf8_lossy ( & bytes) . to_string ( ) )
265
+ }
266
+
267
+ #[ cfg( test) ]
268
+ mod tests;
269
+
270
+ #[ cfg( test) ]
271
+ mod basic_tests {
272
+ use super :: * ;
273
+
274
+ #[ test]
275
+ fn test_sprintf_split_single_value ( ) {
276
+ let values = vec ! [ TagValue :: F64 ( 1.234 ) ] ;
277
+ let result = sprintf_split_values ( "%.3f x %.3f mm" , & values) ;
278
+ assert_eq ! ( result, "1.234 x 1.234 mm" ) ;
279
+ }
280
+
281
+ #[ test]
282
+ fn test_sprintf_split_two_values ( ) {
283
+ let values = vec ! [ TagValue :: F64 ( 1.234 ) , TagValue :: F64 ( 5.678 ) ] ;
284
+ let result = sprintf_split_values ( "%.3f x %.3f mm" , & values) ;
285
+ assert_eq ! ( result, "1.234 x 5.678 mm" ) ;
286
+ }
287
+
288
+ #[ test]
289
+ fn test_sprintf_split_string_values ( ) {
290
+ let values = vec ! [
291
+ TagValue :: String ( "10.5" . to_string( ) ) ,
292
+ TagValue :: String ( "20.3" . to_string( ) )
293
+ ] ;
294
+ let result = sprintf_split_values ( "%.3f x %.3f mm" , & values) ;
295
+ assert_eq ! ( result, "10.5 x 20.3 mm" ) ;
296
+ }
297
+
298
+ #[ test]
299
+ fn test_sprintf_with_string_concat_repeat ( ) {
300
+ // Test the specific failing case: sprintf("%19d %4d %6d" . " %3d %4d %6d" x 8, ...)
301
+ let values = TagValue :: Array ( vec ! [
302
+ TagValue :: I32 ( 1 ) , TagValue :: I32 ( 2 ) , TagValue :: I32 ( 3 ) , // First 3 values
303
+ TagValue :: I32 ( 4 ) , TagValue :: I32 ( 5 ) , TagValue :: I32 ( 6 ) , // Repeated pattern
304
+ TagValue :: I32 ( 7 ) , TagValue :: I32 ( 8 ) , TagValue :: I32 ( 9 ) , // More values...
305
+ ] ) ;
306
+
307
+ let result = sprintf_with_string_concat_repeat (
308
+ "%19d %4d %6d" ,
309
+ " %3d %4d %6d" ,
310
+ 2 , // Test with 2 repetitions for simplicity
311
+ & values
312
+ ) ;
313
+
314
+ // Should build format: "%19d %4d %6d %3d %4d %6d %3d %4d %6d"
315
+ // Expected format string has: 19-width, 4-width, 6-width, then repeated 3-width, 4-width, 6-width pattern
316
+ assert ! ( result. contains( "1" ) ) ; // First value
317
+ assert ! ( result. contains( "2" ) ) ; // Second value
318
+ assert ! ( result. contains( "3" ) ) ; // Third value
319
+ }
320
+
321
+ #[ test]
322
+ fn test_sprintf_concat_repeat_empty_args ( ) {
323
+ // Test edge case with empty args
324
+ let empty_args = TagValue :: Array ( vec ! [ ] ) ;
325
+ let result = sprintf_with_string_concat_repeat (
326
+ "%d" ,
327
+ " %d" ,
328
+ 3 ,
329
+ & empty_args
330
+ ) ;
331
+ // Should handle gracefully even with no args
332
+ assert ! ( !result. is_empty( ) ) ; // sprintf_perl should handle missing args gracefully
333
+ }
334
+
335
+ #[ test]
336
+ fn test_safe_reciprocal ( ) {
337
+ // Normal cases: 1/val
338
+ assert_eq ! ( safe_reciprocal( & TagValue :: I32 ( 2 ) ) , TagValue :: F64 ( 0.5 ) ) ;
339
+ assert_eq ! ( safe_reciprocal( & TagValue :: F64 ( 4.0 ) ) , TagValue :: F64 ( 0.25 ) ) ;
340
+ assert_eq ! ( safe_reciprocal( & TagValue :: U16 ( 10 ) ) , TagValue :: F64 ( 0.1 ) ) ;
341
+
342
+ // Zero cases: should return 0, not infinity
343
+ assert_eq ! ( safe_reciprocal( & TagValue :: I32 ( 0 ) ) , TagValue :: F64 ( 0.0 ) ) ;
344
+ assert_eq ! ( safe_reciprocal( & TagValue :: F64 ( 0.0 ) ) , TagValue :: F64 ( 0.0 ) ) ;
345
+ assert_eq ! ( safe_reciprocal( & TagValue :: String ( "0" . to_string( ) ) ) , TagValue :: F64 ( 0.0 ) ) ;
346
+
347
+ // Empty/falsy cases: should return 0
348
+ assert_eq ! ( safe_reciprocal( & TagValue :: String ( "" . to_string( ) ) ) , TagValue :: F64 ( 0.0 ) ) ;
349
+ assert_eq ! ( safe_reciprocal( & TagValue :: Empty ) , TagValue :: F64 ( 0.0 ) ) ;
350
+
351
+ // String numeric conversion
352
+ assert_eq ! ( safe_reciprocal( & TagValue :: String ( "2.5" . to_string( ) ) ) , TagValue :: F64 ( 0.4 ) ) ;
353
+ assert_eq ! ( safe_reciprocal( & TagValue :: String ( "non-numeric" . to_string( ) ) ) , TagValue :: F64 ( 0.0 ) ) ;
354
+
355
+ // Rational cases
356
+ assert_eq ! ( safe_reciprocal( & TagValue :: Rational ( 8 , 2 ) ) , TagValue :: F64 ( 0.25 ) ) ; // 1/(8/2) = 1/4
357
+ assert_eq ! ( safe_reciprocal( & TagValue :: Rational ( 0 , 1 ) ) , TagValue :: F64 ( 0.0 ) ) ; // 0 numerator
358
+ assert_eq ! ( safe_reciprocal( & TagValue :: SRational ( 6 , 3 ) ) , TagValue :: F64 ( 0.5 ) ) ; // 1/(6/3) = 1/2
359
+ }
360
+ }
0 commit comments