dry4rust/threshold.rs
1// Copyright (c) 2026 Matjaz Domen Pecan
2// Copyright 2026 Umberto Gotti <umberto.gotti@umbertogotti.dev>
3// Licensed under the MIT License
4// SPDX-License-Identifier: MIT
5
6use crate::error::Error;
7use crate::error::Result;
8
9/// A proportion between none and all, held as a fraction.
10///
11/// Every threshold this tool takes is one of these: a similarity score to
12/// clear, or a share of duplicated lines to stay under. They differ only in
13/// how they are written -- `0.85` in one place, `85.0` in another -- so they
14/// are one type with two constructors rather than two types.
15///
16/// The invariant is the point. A similarity threshold of `5` makes the size
17/// pre-filter reject every pair, and the report then says "no near
18/// duplicates" -- a silence indistinguishable from a clean codebase. Nothing
19/// downstream can tell that apart, so the value never gets that far.
20#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
21pub struct Threshold(f64);
22
23impl Threshold {
24 /// The similarity threshold used when nothing sets one.
25 pub const DEFAULT_SIMILARITY: Self = Self(0.9);
26
27 /// A threshold written as a fraction, as `similarity_threshold` is.
28 ///
29 /// # Errors
30 ///
31 /// Returns [`Error::InvalidConfig`] naming `field` when `value` is
32 /// outside `0.0..=1.0`, which `NaN` also is.
33 pub fn fraction(field: &'static str, value: f64) -> Result<Self> {
34 if (0.0..=1.0).contains(&value) {
35 Ok(Self(value))
36 } else {
37 Err(Error::InvalidConfig {
38 field,
39 value: value.to_string(),
40 expected: "a fraction between 0.0 and 1.0",
41 })
42 }
43 }
44
45 /// A threshold written as a percentage, as the `check` ceilings are.
46 ///
47 /// # Errors
48 ///
49 /// Returns [`Error::InvalidConfig`] naming `field` when `value` is
50 /// outside `0.0..=100.0`, which `NaN` also is.
51 pub fn percent(field: &'static str, value: f64) -> Result<Self> {
52 if (0.0..=100.0).contains(&value) {
53 Ok(Self(value / 100.0))
54 } else {
55 Err(Error::InvalidConfig {
56 field,
57 value: value.to_string(),
58 expected: "a percentage between 0.0 and 100.0",
59 })
60 }
61 }
62
63 /// The threshold as a fraction of one.
64 #[must_use]
65 pub const fn as_fraction(self) -> f64 {
66 self.0
67 }
68
69 /// The threshold as a share of a hundred.
70 #[must_use]
71 pub fn as_percent(self) -> f64 {
72 self.0 * 100.0
73 }
74}