1#[derive(Debug, Clone, Copy, PartialEq)]
38pub enum Formatter {
39 Float {
41 precision: usize,
43 },
44
45 Decibel {
50 precision: usize,
52 },
53
54 DecibelDirect {
59 precision: usize,
61 min_db: f64,
63 },
64
65 Frequency,
69
70 Milliseconds {
74 precision: usize,
76 },
77
78 Seconds {
82 precision: usize,
84 },
85
86 Percent {
91 precision: usize,
93 },
94
95 Pan,
100
101 Ratio {
105 precision: usize,
107 },
108
109 Semitones,
113
114 Boolean,
118}
119
120impl Formatter {
121 pub fn text(&self, value: f64) -> String {
134 match self {
135 Formatter::Float { precision } => {
136 format!("{:.prec$}", value, prec = *precision)
137 }
138
139 Formatter::Decibel { precision } => {
140 if value < 1e-10 {
141 "-inf".to_string()
142 } else {
143 let db = 20.0 * value.log10();
144 if db >= 0.0 {
145 format!("+{:.prec$}", db, prec = *precision)
146 } else {
147 format!("{:.prec$}", db, prec = *precision)
148 }
149 }
150 }
151
152 Formatter::DecibelDirect { precision, min_db } => {
153 if value < *min_db {
156 "-inf".to_string()
157 } else if value >= 0.0 {
158 format!("+{:.prec$}", value, prec = *precision)
159 } else {
160 format!("{:.prec$}", value, prec = *precision)
161 }
162 }
163
164 Formatter::Frequency => {
165 if value >= 1000.0 {
166 format!("{:.2}k", value / 1000.0)
167 } else if value >= 100.0 {
168 format!("{:.0}", value)
169 } else {
170 format!("{:.1}", value)
171 }
172 }
173
174 Formatter::Milliseconds { precision } => {
175 format!("{:.prec$}", value, prec = *precision)
176 }
177
178 Formatter::Seconds { precision } => {
179 format!("{:.prec$}", value, prec = *precision)
180 }
181
182 Formatter::Percent { precision } => {
183 format!("{:.prec$}", value * 100.0, prec = *precision)
184 }
185
186 Formatter::Pan => {
187 if value.abs() < 0.005 {
188 "C".to_string()
189 } else if value < 0.0 {
190 format!("L{:.0}", value.abs() * 100.0)
191 } else {
192 format!("R{:.0}", value * 100.0)
193 }
194 }
195
196 Formatter::Ratio { precision } => {
197 if value > 100.0 {
198 "∞:1".to_string()
199 } else {
200 format!("{:.prec$}:1", value, prec = *precision)
201 }
202 }
203
204 Formatter::Semitones => {
205 let st = value.round() as i64;
206 if st > 0 {
207 format!("+{}", st)
208 } else {
209 format!("{}", st)
210 }
211 }
212
213 Formatter::Boolean => {
214 if value > 0.5 {
215 "On".to_string()
216 } else {
217 "Off".to_string()
218 }
219 }
220 }
221 }
222
223 pub fn parse(&self, s: &str) -> Option<f64> {
228 let s = s.trim();
229
230 match self {
231 Formatter::Float { .. } => s.parse().ok(),
232
233 Formatter::Decibel { .. } => {
234 let trimmed = s
235 .trim_end_matches(" dB")
236 .trim_end_matches("dB")
237 .trim();
238
239 if trimmed.eq_ignore_ascii_case("-inf")
240 || trimmed.eq_ignore_ascii_case("-∞")
241 || trimmed == "-infinity"
242 {
243 return Some(0.0);
244 }
245
246 let db: f64 = trimmed.parse().ok()?;
247 Some(10.0_f64.powf(db / 20.0))
248 }
249
250 Formatter::DecibelDirect { min_db, .. } => {
251 let trimmed = s
253 .trim_end_matches(" dB")
254 .trim_end_matches("dB")
255 .trim();
256
257 if trimmed.eq_ignore_ascii_case("-inf")
258 || trimmed.eq_ignore_ascii_case("-∞")
259 || trimmed == "-infinity"
260 {
261 return Some(*min_db);
262 }
263
264 trimmed.parse().ok()
265 }
266
267 Formatter::Frequency => {
268 if let Some(khz_str) = s
270 .strip_suffix(" kHz")
271 .or_else(|| s.strip_suffix("kHz"))
272 .or_else(|| s.strip_suffix(" khz"))
273 .or_else(|| s.strip_suffix("khz"))
274 {
275 return khz_str.trim().parse::<f64>().ok().map(|v| v * 1000.0);
276 }
277
278 let hz_str = s
280 .trim_end_matches(" Hz")
281 .trim_end_matches("Hz")
282 .trim_end_matches(" hz")
283 .trim_end_matches("hz")
284 .trim();
285
286 hz_str.parse().ok()
287 }
288
289 Formatter::Milliseconds { .. } => {
290 let trimmed = s
291 .strip_suffix(" ms")
292 .or_else(|| s.strip_suffix("ms"))
293 .unwrap_or(s)
294 .trim();
295 trimmed.parse().ok()
296 }
297
298 Formatter::Seconds { .. } => {
299 let trimmed = s
300 .strip_suffix(" s")
301 .or_else(|| s.strip_suffix("s"))
302 .unwrap_or(s)
303 .trim();
304 trimmed.parse().ok()
305 }
306
307 Formatter::Percent { .. } => {
308 let trimmed = s.trim_end_matches('%').trim();
309 trimmed.parse::<f64>().ok().map(|v| v / 100.0)
310 }
311
312 Formatter::Pan => {
313 let s_upper = s.to_uppercase();
314 if s_upper == "C" || s_upper == "CENTER" || s_upper == "0" {
315 return Some(0.0);
316 }
317
318 if let Some(left) = s_upper.strip_prefix('L') {
319 return left.trim().parse::<f64>().ok().map(|v| -v / 100.0);
320 }
321
322 if let Some(right) = s_upper.strip_prefix('R') {
323 return right.trim().parse::<f64>().ok().map(|v| v / 100.0);
324 }
325
326 if let Ok(v) = s.parse::<f64>() {
328 if v.abs() > 1.0 {
329 return Some(v / 100.0); }
331 return Some(v); }
333
334 None
335 }
336
337 Formatter::Ratio { .. } => {
338 if s == "∞:1" || s == "inf:1" || s.eq_ignore_ascii_case("infinity:1") {
340 return Some(f64::INFINITY);
341 }
342
343 let trimmed = s.trim_end_matches(":1").trim();
345 trimmed.parse().ok()
346 }
347
348 Formatter::Semitones => {
349 let trimmed = s.trim_end_matches(" st").trim_end_matches("st").trim();
350 trimmed.parse().ok()
351 }
352
353 Formatter::Boolean => match s.to_lowercase().as_str() {
354 "on" | "true" | "yes" | "1" | "enabled" => Some(1.0),
355 "off" | "false" | "no" | "0" | "disabled" => Some(0.0),
356 _ => None,
357 },
358 }
359 }
360
361 pub fn unit(&self) -> &'static str {
363 match self {
364 Formatter::Float { .. } => "",
365 Formatter::Decibel { .. } => "dB",
366 Formatter::DecibelDirect { .. } => "dB",
367 Formatter::Frequency => "Hz",
368 Formatter::Milliseconds { .. } => "ms",
369 Formatter::Seconds { .. } => "s",
370 Formatter::Percent { .. } => "%",
371 Formatter::Pan => "",
372 Formatter::Ratio { .. } => "",
373 Formatter::Semitones => "st",
374 Formatter::Boolean => "",
375 }
376 }
377}
378
379impl Default for Formatter {
380 fn default() -> Self {
381 Formatter::Float { precision: 2 }
382 }
383}
384
385impl Formatter {
386 pub fn with_precision(self, precision: usize) -> Self {
405 match self {
406 Formatter::Float { .. } => Formatter::Float { precision },
407 Formatter::Decibel { .. } => Formatter::Decibel { precision },
408 Formatter::DecibelDirect { min_db, .. } => {
409 Formatter::DecibelDirect { precision, min_db }
410 }
411 Formatter::Milliseconds { .. } => Formatter::Milliseconds { precision },
412 Formatter::Seconds { .. } => Formatter::Seconds { precision },
413 Formatter::Percent { .. } => Formatter::Percent { precision },
414 Formatter::Ratio { .. } => Formatter::Ratio { precision },
415 Formatter::Frequency
417 | Formatter::Pan
418 | Formatter::Semitones
419 | Formatter::Boolean => self,
420 }
421 }
422
423 pub fn supports_precision(&self) -> bool {
427 matches!(
428 self,
429 Formatter::Float { .. }
430 | Formatter::Decibel { .. }
431 | Formatter::DecibelDirect { .. }
432 | Formatter::Milliseconds { .. }
433 | Formatter::Seconds { .. }
434 | Formatter::Percent { .. }
435 | Formatter::Ratio { .. }
436 )
437 }
438
439 pub fn precision(&self) -> Option<usize> {
444 match self {
445 Formatter::Float { precision }
446 | Formatter::Decibel { precision }
447 | Formatter::DecibelDirect { precision, .. }
448 | Formatter::Milliseconds { precision }
449 | Formatter::Seconds { precision }
450 | Formatter::Percent { precision }
451 | Formatter::Ratio { precision } => Some(*precision),
452 Formatter::Frequency | Formatter::Pan | Formatter::Semitones | Formatter::Boolean => {
453 None
454 }
455 }
456 }
457}
458
459#[cfg(test)]
460mod tests {
461 use super::*;
462
463 #[test]
464 fn test_with_precision_float() {
465 let formatter = Formatter::Float { precision: 2 };
466 let updated = formatter.with_precision(4);
467 assert_eq!(updated.precision(), Some(4));
468 assert_eq!(updated.text(1.2345), "1.2345");
469 }
470
471 #[test]
472 fn test_with_precision_decibel() {
473 let formatter = Formatter::Decibel { precision: 1 };
474 let updated = formatter.with_precision(2);
475 assert_eq!(updated.precision(), Some(2));
476 assert_eq!(updated.text(1.0), "+0.00"); }
478
479 #[test]
480 fn test_with_precision_decibel_direct() {
481 let formatter = Formatter::DecibelDirect {
482 precision: 1,
483 min_db: -60.0,
484 };
485 let updated = formatter.with_precision(3);
486 assert_eq!(updated.precision(), Some(3));
487 if let Formatter::DecibelDirect { min_db, precision } = updated {
489 assert_eq!(min_db, -60.0);
490 assert_eq!(precision, 3);
491 } else {
492 panic!("Expected DecibelDirect variant");
493 }
494 assert_eq!(updated.text(-6.5), "-6.500");
495 }
496
497 #[test]
498 fn test_with_precision_milliseconds() {
499 let formatter = Formatter::Milliseconds { precision: 1 };
500 let updated = formatter.with_precision(0);
501 assert_eq!(updated.precision(), Some(0));
502 assert_eq!(updated.text(10.5), "10"); }
504
505 #[test]
506 fn test_with_precision_seconds() {
507 let formatter = Formatter::Seconds { precision: 2 };
508 let updated = formatter.with_precision(3);
509 assert_eq!(updated.precision(), Some(3));
510 assert_eq!(updated.text(1.5), "1.500");
511 }
512
513 #[test]
514 fn test_with_precision_percent() {
515 let formatter = Formatter::Percent { precision: 0 };
516 let updated = formatter.with_precision(1);
517 assert_eq!(updated.precision(), Some(1));
518 assert_eq!(updated.text(0.755), "75.5"); }
520
521 #[test]
522 fn test_with_precision_ratio() {
523 let formatter = Formatter::Ratio { precision: 1 };
524 let updated = formatter.with_precision(2);
525 assert_eq!(updated.precision(), Some(2));
526 assert_eq!(updated.text(4.0), "4.00:1");
527 }
528
529 #[test]
530 fn test_with_precision_no_effect_on_frequency() {
531 let formatter = Formatter::Frequency;
532 let updated = formatter.with_precision(5);
533 assert_eq!(updated, Formatter::Frequency);
534 assert_eq!(updated.precision(), None);
535 }
536
537 #[test]
538 fn test_with_precision_no_effect_on_pan() {
539 let formatter = Formatter::Pan;
540 let updated = formatter.with_precision(3);
541 assert_eq!(updated, Formatter::Pan);
542 assert_eq!(updated.precision(), None);
543 }
544
545 #[test]
546 fn test_with_precision_no_effect_on_semitones() {
547 let formatter = Formatter::Semitones;
548 let updated = formatter.with_precision(2);
549 assert_eq!(updated, Formatter::Semitones);
550 assert_eq!(updated.precision(), None);
551 }
552
553 #[test]
554 fn test_with_precision_no_effect_on_boolean() {
555 let formatter = Formatter::Boolean;
556 let updated = formatter.with_precision(1);
557 assert_eq!(updated, Formatter::Boolean);
558 assert_eq!(updated.precision(), None);
559 }
560
561 #[test]
562 fn test_supports_precision() {
563 assert!(Formatter::Float { precision: 2 }.supports_precision());
564 assert!(Formatter::Decibel { precision: 1 }.supports_precision());
565 assert!(Formatter::DecibelDirect {
566 precision: 1,
567 min_db: -60.0
568 }
569 .supports_precision());
570 assert!(Formatter::Milliseconds { precision: 1 }.supports_precision());
571 assert!(Formatter::Seconds { precision: 2 }.supports_precision());
572 assert!(Formatter::Percent { precision: 0 }.supports_precision());
573 assert!(Formatter::Ratio { precision: 1 }.supports_precision());
574
575 assert!(!Formatter::Frequency.supports_precision());
576 assert!(!Formatter::Pan.supports_precision());
577 assert!(!Formatter::Semitones.supports_precision());
578 assert!(!Formatter::Boolean.supports_precision());
579 }
580
581 #[test]
582 fn test_precision_getter() {
583 assert_eq!(Formatter::Float { precision: 3 }.precision(), Some(3));
584 assert_eq!(Formatter::Decibel { precision: 2 }.precision(), Some(2));
585 assert_eq!(
586 Formatter::DecibelDirect {
587 precision: 1,
588 min_db: -60.0
589 }
590 .precision(),
591 Some(1)
592 );
593 assert_eq!(Formatter::Frequency.precision(), None);
594 assert_eq!(Formatter::Pan.precision(), None);
595 }
596}