Skip to main content

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}