Skip to main content

nautilus_core/
correctness.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Functions for correctness checks similar to the *design by contract* philosophy.
17//!
18//! This module provides validation checking of function or method conditions.
19//!
20//! A condition is a predicate which must be true just prior to the execution of
21//! some section of code - for correct behavior as per the design specification.
22//!
23//! A typed [`Result`] is returned with a descriptive message when the condition
24//! check fails.
25
26use std::fmt::{Debug, Display};
27
28use rust_decimal::Decimal;
29use thiserror::Error;
30
31use crate::collections::{MapLike, SetLike};
32
33/// A message prefix that can be used with calls to `expect` or other assertion-related functions.
34///
35/// This constant provides a standard message that can be used to indicate a failure condition
36/// when a predicate or condition does not hold true. It is typically used in conjunction with
37/// functions like `expect` to provide a consistent error message.
38pub const FAILED: &str = "Condition failed";
39
40/// Error type for correctness checks.
41#[derive(Clone, Debug, Error, Eq, PartialEq)]
42pub enum CorrectnessError {
43    /// A predicate or invariant check failed.
44    #[error("{message}")]
45    PredicateViolation {
46        /// The failure message.
47        message: String,
48    },
49    /// A string was empty.
50    #[error("invalid string for '{param}', was empty")]
51    EmptyString {
52        /// The parameter name.
53        param: String,
54    },
55    /// A string was all whitespace.
56    #[error("invalid string for '{param}', was all whitespace")]
57    WhitespaceString {
58        /// The parameter name.
59        param: String,
60    },
61    /// A string contained a non-ASCII character.
62    #[error("invalid string for '{param}' contained a non-ASCII char, was '{value}'")]
63    NonAsciiString {
64        /// The parameter name.
65        param: String,
66        /// The provided value.
67        value: String,
68    },
69    /// A string did not contain an expected pattern.
70    #[error("invalid string for '{param}' did not contain '{pattern}', was '{value}'")]
71    MissingSubstring {
72        /// The parameter name.
73        param: String,
74        /// The expected substring.
75        pattern: String,
76        /// The provided value.
77        value: String,
78    },
79    /// Two values were not equal.
80    #[error(
81        "'{lhs_param}' {type_name} of {lhs} was not equal to '{rhs_param}' {type_name} of {rhs}"
82    )]
83    EqualityMismatch {
84        /// The left parameter name.
85        lhs_param: String,
86        /// The right parameter name.
87        rhs_param: String,
88        /// The left value.
89        lhs: String,
90        /// The right value.
91        rhs: String,
92        /// The displayed type name.
93        type_name: &'static str,
94    },
95    /// A value that must be positive was not positive.
96    #[error("invalid {type_name} for '{param}' not positive, was {value}")]
97    NotPositive {
98        /// The parameter name.
99        param: String,
100        /// The provided value.
101        value: String,
102        /// The displayed type name.
103        type_name: &'static str,
104    },
105    /// A value that must not be negative was negative.
106    #[error("invalid {type_name} for '{param}' negative, was {value}")]
107    NegativeValue {
108        /// The parameter name.
109        param: String,
110        /// The provided value.
111        value: String,
112        /// The displayed type name.
113        type_name: &'static str,
114    },
115    /// A value was invalid for its type.
116    #[error("invalid {type_name} for '{param}', was {value}")]
117    InvalidValue {
118        /// The parameter name.
119        param: String,
120        /// The provided value.
121        value: String,
122        /// The displayed type name.
123        type_name: &'static str,
124    },
125    /// A value was outside an inclusive range.
126    #[error("invalid {type_name} for '{param}' not in range [{min}, {max}], was {value}")]
127    OutOfRange {
128        /// The parameter name.
129        param: String,
130        /// The lower bound.
131        min: String,
132        /// The upper bound.
133        max: String,
134        /// The provided value.
135        value: String,
136        /// The displayed type name.
137        type_name: &'static str,
138    },
139    /// A collection that must be empty was not empty.
140    #[error("the '{param}' {collection_kind} `{type_repr}` was not empty")]
141    CollectionNotEmpty {
142        /// The parameter name.
143        param: String,
144        /// The collection kind.
145        collection_kind: &'static str,
146        /// The collection type representation.
147        type_repr: String,
148    },
149    /// A collection that must not be empty was empty.
150    #[error("the '{param}' {collection_kind} `{type_repr}` was empty")]
151    CollectionEmpty {
152        /// The parameter name.
153        param: String,
154        /// The collection kind.
155        collection_kind: &'static str,
156        /// The collection type representation.
157        type_repr: String,
158    },
159    /// A map key was already present.
160    #[error("the '{key_name}' key {key} was already in the '{map_name}' map `{map_type_repr}`")]
161    KeyPresent {
162        /// The key parameter name.
163        key_name: String,
164        /// The map parameter name.
165        map_name: String,
166        /// The key value.
167        key: String,
168        /// The map type representation.
169        map_type_repr: String,
170    },
171    /// A map key was missing.
172    #[error("the '{key_name}' key {key} was not in the '{map_name}' map `{map_type_repr}`")]
173    KeyMissing {
174        /// The key parameter name.
175        key_name: String,
176        /// The map parameter name.
177        map_name: String,
178        /// The key value.
179        key: String,
180        /// The map type representation.
181        map_type_repr: String,
182    },
183    /// A set member was already present.
184    #[error("the '{member_name}' member was already in the '{set_name}' set `{set_type_repr}`")]
185    MemberPresent {
186        /// The member parameter name.
187        member_name: String,
188        /// The set parameter name.
189        set_name: String,
190        /// The set type representation.
191        set_type_repr: String,
192    },
193    /// A set member was missing.
194    #[error("the '{member_name}' member was not in the '{set_name}' set `{set_type_repr}`")]
195    MemberMissing {
196        /// The member parameter name.
197        member_name: String,
198        /// The set parameter name.
199        set_name: String,
200        /// The set type representation.
201        set_type_repr: String,
202    },
203}
204
205/// Result type for correctness checks.
206pub type Result<T> = std::result::Result<T, CorrectnessError>;
207
208/// Result type alias for APIs that want to name the correctness error domain explicitly.
209pub type CorrectnessResult<T> = Result<T>;
210
211/// Extension trait for [`CorrectnessResult`] that panics with the error's
212/// [`Display`] form rather than its `Debug` form.
213///
214/// Use this instead of [`std::result::Result::expect`] when unwrapping a
215/// correctness result: `expect` formats the error with `{:?}`, which exposes
216/// the internal [`CorrectnessError`] struct layout in panic output, while
217/// [`CorrectnessResultExt::expect_display`] preserves the human-readable
218/// message defined on each variant.
219pub trait CorrectnessResultExt<T> {
220    /// Returns the contained [`Ok`] value, panicking with `msg: <error display>`
221    /// on [`Err`].
222    fn expect_display(self, msg: &str) -> T;
223}
224
225impl<T> CorrectnessResultExt<T> for CorrectnessResult<T> {
226    #[inline]
227    #[track_caller]
228    fn expect_display(self, msg: &str) -> T {
229        match self {
230            Ok(value) => value,
231            Err(e) => panic!("{msg}: {e}"),
232        }
233    }
234}
235
236/// Checks the `predicate` is true.
237///
238/// # Errors
239///
240/// Returns an error if the validation check fails.
241#[inline(always)]
242pub fn check_predicate_true(predicate: bool, fail_msg: &str) -> Result<()> {
243    if !predicate {
244        return Err(CorrectnessError::PredicateViolation {
245            message: fail_msg.to_string(),
246        });
247    }
248    Ok(())
249}
250
251/// Checks the `predicate` is false.
252///
253/// # Errors
254///
255/// Returns an error if the validation check fails.
256#[inline(always)]
257pub fn check_predicate_false(predicate: bool, fail_msg: &str) -> Result<()> {
258    if predicate {
259        return Err(CorrectnessError::PredicateViolation {
260            message: fail_msg.to_string(),
261        });
262    }
263    Ok(())
264}
265
266/// Checks if the string `s` is not empty.
267///
268/// This function performs a basic check to ensure the string has at least one character.
269/// Unlike `check_valid_string`, it does not validate ASCII characters or check for whitespace.
270///
271/// # Errors
272///
273/// Returns an error if `s` is empty.
274#[inline(always)]
275pub fn check_nonempty_string<T: AsRef<str>>(s: T, param: &str) -> Result<()> {
276    if s.as_ref().is_empty() {
277        return Err(CorrectnessError::EmptyString {
278            param: param.to_string(),
279        });
280    }
281    Ok(())
282}
283
284/// Checks the string `s` has semantic meaning and contains only ASCII characters.
285///
286/// # Errors
287///
288/// Returns an error if:
289/// - `s` is an empty string.
290/// - `s` consists solely of whitespace characters.
291/// - `s` contains one or more non-ASCII characters.
292#[inline(always)]
293pub fn check_valid_string_ascii<T: AsRef<str>>(s: T, param: &str) -> Result<()> {
294    let s = s.as_ref();
295
296    if s.is_empty() {
297        return Err(CorrectnessError::EmptyString {
298            param: param.to_string(),
299        });
300    }
301
302    // Ensure string is only traversed once
303    let mut has_non_whitespace = false;
304
305    for c in s.chars() {
306        if !c.is_whitespace() {
307            has_non_whitespace = true;
308        }
309
310        if !c.is_ascii() {
311            return Err(CorrectnessError::NonAsciiString {
312                param: param.to_string(),
313                value: s.to_string(),
314            });
315        }
316    }
317
318    if !has_non_whitespace {
319        return Err(CorrectnessError::WhitespaceString {
320            param: param.to_string(),
321        });
322    }
323
324    Ok(())
325}
326
327/// Checks the string `s` has semantic meaning and allows UTF-8 characters.
328///
329/// This is a relaxed version of [`check_valid_string_ascii`] that permits non-ASCII UTF-8 characters.
330/// Use this for external identifiers (e.g., exchange symbols) that may contain Unicode characters.
331///
332/// # Errors
333///
334/// Returns an error if:
335/// - `s` is an empty string.
336/// - `s` consists solely of whitespace characters.
337#[inline(always)]
338pub fn check_valid_string_utf8<T: AsRef<str>>(s: T, param: &str) -> Result<()> {
339    let s = s.as_ref();
340
341    if s.is_empty() {
342        return Err(CorrectnessError::EmptyString {
343            param: param.to_string(),
344        });
345    }
346
347    let has_non_whitespace = s.chars().any(|c| !c.is_whitespace());
348
349    if !has_non_whitespace {
350        return Err(CorrectnessError::WhitespaceString {
351            param: param.to_string(),
352        });
353    }
354
355    Ok(())
356}
357
358/// Checks the string `s` if Some, contains only ASCII characters and has semantic meaning.
359///
360/// # Errors
361///
362/// Returns an error if:
363/// - `s` is an empty string.
364/// - `s` consists solely of whitespace characters.
365/// - `s` contains one or more non-ASCII characters.
366#[inline(always)]
367pub fn check_valid_string_ascii_optional<T: AsRef<str>>(s: Option<T>, param: &str) -> Result<()> {
368    if let Some(s) = s {
369        check_valid_string_ascii(s, param)?;
370    }
371    Ok(())
372}
373
374/// Checks the string `s` contains the pattern `pat`.
375///
376/// # Errors
377///
378/// Returns an error if the validation check fails.
379#[inline(always)]
380pub fn check_string_contains<T: AsRef<str>>(s: T, pat: &str, param: &str) -> Result<()> {
381    let s = s.as_ref();
382    if !s.contains(pat) {
383        return Err(CorrectnessError::MissingSubstring {
384            param: param.to_string(),
385            pattern: pat.to_string(),
386            value: s.to_string(),
387        });
388    }
389    Ok(())
390}
391
392/// Checks the values are equal.
393///
394/// # Errors
395///
396/// Returns an error if the validation check fails.
397#[inline(always)]
398pub fn check_equal<T: PartialEq + Debug + Display>(
399    lhs: &T,
400    rhs: &T,
401    lhs_param: &str,
402    rhs_param: &str,
403) -> Result<()> {
404    if lhs != rhs {
405        return Err(CorrectnessError::EqualityMismatch {
406            lhs_param: lhs_param.to_string(),
407            rhs_param: rhs_param.to_string(),
408            lhs: lhs.to_string(),
409            rhs: rhs.to_string(),
410            type_name: "value",
411        });
412    }
413    Ok(())
414}
415
416/// Checks the `u8` values are equal.
417///
418/// # Errors
419///
420/// Returns an error if the validation check fails.
421#[inline(always)]
422pub fn check_equal_u8(lhs: u8, rhs: u8, lhs_param: &str, rhs_param: &str) -> Result<()> {
423    if lhs != rhs {
424        return Err(CorrectnessError::EqualityMismatch {
425            lhs_param: lhs_param.to_string(),
426            rhs_param: rhs_param.to_string(),
427            lhs: lhs.to_string(),
428            rhs: rhs.to_string(),
429            type_name: "u8",
430        });
431    }
432    Ok(())
433}
434
435/// Checks the `usize` values are equal.
436///
437/// # Errors
438///
439/// Returns an error if the validation check fails.
440#[inline(always)]
441pub fn check_equal_usize(lhs: usize, rhs: usize, lhs_param: &str, rhs_param: &str) -> Result<()> {
442    if lhs != rhs {
443        return Err(CorrectnessError::EqualityMismatch {
444            lhs_param: lhs_param.to_string(),
445            rhs_param: rhs_param.to_string(),
446            lhs: lhs.to_string(),
447            rhs: rhs.to_string(),
448            type_name: "usize",
449        });
450    }
451    Ok(())
452}
453
454/// Checks the `u64` value is positive (> 0).
455///
456/// # Errors
457///
458/// Returns an error if the validation check fails.
459#[inline(always)]
460pub fn check_positive_u64(value: u64, param: &str) -> Result<()> {
461    if value == 0 {
462        return Err(CorrectnessError::NotPositive {
463            param: param.to_string(),
464            value: value.to_string(),
465            type_name: "u64",
466        });
467    }
468    Ok(())
469}
470
471/// Checks the `u128` value is positive (> 0).
472///
473/// # Errors
474///
475/// Returns an error if the validation check fails.
476#[inline(always)]
477pub fn check_positive_u128(value: u128, param: &str) -> Result<()> {
478    if value == 0 {
479        return Err(CorrectnessError::NotPositive {
480            param: param.to_string(),
481            value: value.to_string(),
482            type_name: "u128",
483        });
484    }
485    Ok(())
486}
487
488/// Checks the `i64` value is positive (> 0).
489///
490/// # Errors
491///
492/// Returns an error if the validation check fails.
493#[inline(always)]
494pub fn check_positive_i64(value: i64, param: &str) -> Result<()> {
495    if value <= 0 {
496        return Err(CorrectnessError::NotPositive {
497            param: param.to_string(),
498            value: value.to_string(),
499            type_name: "i64",
500        });
501    }
502    Ok(())
503}
504
505/// Checks the `i64` value is positive (> 0).
506///
507/// # Errors
508///
509/// Returns an error if the validation check fails.
510#[inline(always)]
511pub fn check_positive_i128(value: i128, param: &str) -> Result<()> {
512    if value <= 0 {
513        return Err(CorrectnessError::NotPositive {
514            param: param.to_string(),
515            value: value.to_string(),
516            type_name: "i128",
517        });
518    }
519    Ok(())
520}
521
522/// Checks the `f64` value is non-negative (>= 0).
523///
524/// # Errors
525///
526/// Returns an error if the validation check fails.
527#[inline(always)]
528pub fn check_non_negative_f64(value: f64, param: &str) -> Result<()> {
529    if value.is_nan() || value.is_infinite() {
530        return Err(CorrectnessError::InvalidValue {
531            param: param.to_string(),
532            value: value.to_string(),
533            type_name: "f64",
534        });
535    }
536
537    if value < 0.0 {
538        return Err(CorrectnessError::NegativeValue {
539            param: param.to_string(),
540            value: value.to_string(),
541            type_name: "f64",
542        });
543    }
544    Ok(())
545}
546
547/// Checks the `u8` value is in range [`l`, `r`] (inclusive).
548///
549/// # Errors
550///
551/// Returns an error if the validation check fails.
552#[inline(always)]
553pub fn check_in_range_inclusive_u8(value: u8, l: u8, r: u8, param: &str) -> Result<()> {
554    if value < l || value > r {
555        return Err(CorrectnessError::OutOfRange {
556            param: param.to_string(),
557            min: l.to_string(),
558            max: r.to_string(),
559            value: value.to_string(),
560            type_name: "u8",
561        });
562    }
563    Ok(())
564}
565
566/// Checks the `u64` value is range [`l`, `r`] (inclusive).
567///
568/// # Errors
569///
570/// Returns an error if the validation check fails.
571#[inline(always)]
572pub fn check_in_range_inclusive_u64(value: u64, l: u64, r: u64, param: &str) -> Result<()> {
573    if value < l || value > r {
574        return Err(CorrectnessError::OutOfRange {
575            param: param.to_string(),
576            min: l.to_string(),
577            max: r.to_string(),
578            value: value.to_string(),
579            type_name: "u64",
580        });
581    }
582    Ok(())
583}
584
585/// Checks the `i64` value is in range [`l`, `r`] (inclusive).
586///
587/// # Errors
588///
589/// Returns an error if the validation check fails.
590#[inline(always)]
591pub fn check_in_range_inclusive_i64(value: i64, l: i64, r: i64, param: &str) -> Result<()> {
592    if value < l || value > r {
593        return Err(CorrectnessError::OutOfRange {
594            param: param.to_string(),
595            min: l.to_string(),
596            max: r.to_string(),
597            value: value.to_string(),
598            type_name: "i64",
599        });
600    }
601    Ok(())
602}
603
604/// Checks the `f64` value is in range [`l`, `r`] (inclusive).
605///
606/// # Errors
607///
608/// Returns an error if the validation check fails.
609#[inline(always)]
610pub fn check_in_range_inclusive_f64(value: f64, l: f64, r: f64, param: &str) -> Result<()> {
611    // Hardcoded epsilon is intentional and appropriate here because:
612    // - 1e-15 is conservative for IEEE 754 double precision (machine epsilon ~2.22e-16)
613    // - This function is used for validation, not high-precision calculations
614    // - The epsilon prevents spurious failures due to floating-point representation
615    // - Making it configurable would complicate the API for minimal benefit
616    const EPSILON: f64 = 1e-15;
617
618    if value.is_nan() || value.is_infinite() {
619        return Err(CorrectnessError::InvalidValue {
620            param: param.to_string(),
621            value: value.to_string(),
622            type_name: "f64",
623        });
624    }
625
626    if value < l - EPSILON || value > r + EPSILON {
627        return Err(CorrectnessError::OutOfRange {
628            param: param.to_string(),
629            min: l.to_string(),
630            max: r.to_string(),
631            value: value.to_string(),
632            type_name: "f64",
633        });
634    }
635    Ok(())
636}
637
638/// Checks the `usize` value is in range [`l`, `r`] (inclusive).
639///
640/// # Errors
641///
642/// Returns an error if the validation check fails.
643#[inline(always)]
644pub fn check_in_range_inclusive_usize(value: usize, l: usize, r: usize, param: &str) -> Result<()> {
645    if value < l || value > r {
646        return Err(CorrectnessError::OutOfRange {
647            param: param.to_string(),
648            min: l.to_string(),
649            max: r.to_string(),
650            value: value.to_string(),
651            type_name: "usize",
652        });
653    }
654    Ok(())
655}
656
657/// Checks the slice is empty.
658///
659/// # Errors
660///
661/// Returns an error if the validation check fails.
662#[inline(always)]
663pub fn check_slice_empty<T>(slice: &[T], param: &str) -> Result<()> {
664    if !slice.is_empty() {
665        return Err(CorrectnessError::CollectionNotEmpty {
666            param: param.to_string(),
667            collection_kind: "slice",
668            type_repr: slice_type_repr::<T>(),
669        });
670    }
671    Ok(())
672}
673
674/// Checks the slice is **not** empty.
675///
676/// # Errors
677///
678/// Returns an error if the validation check fails.
679#[inline(always)]
680pub fn check_slice_not_empty<T>(slice: &[T], param: &str) -> Result<()> {
681    if slice.is_empty() {
682        return Err(CorrectnessError::CollectionEmpty {
683            param: param.to_string(),
684            collection_kind: "slice",
685            type_repr: slice_type_repr::<T>(),
686        });
687    }
688    Ok(())
689}
690
691/// Checks the hashmap is empty.
692///
693/// # Errors
694///
695/// Returns an error if the validation check fails.
696#[inline(always)]
697pub fn check_map_empty<M>(map: &M, param: &str) -> Result<()>
698where
699    M: MapLike,
700{
701    if !map.is_empty() {
702        return Err(CorrectnessError::CollectionNotEmpty {
703            param: param.to_string(),
704            collection_kind: "map",
705            type_repr: map_type_repr::<M>(),
706        });
707    }
708    Ok(())
709}
710
711/// Checks the map is **not** empty.
712///
713/// # Errors
714///
715/// Returns an error if the validation check fails.
716#[inline(always)]
717pub fn check_map_not_empty<M>(map: &M, param: &str) -> Result<()>
718where
719    M: MapLike,
720{
721    if map.is_empty() {
722        return Err(CorrectnessError::CollectionEmpty {
723            param: param.to_string(),
724            collection_kind: "map",
725            type_repr: map_type_repr::<M>(),
726        });
727    }
728    Ok(())
729}
730
731/// Checks the `key` is **not** in the `map`.
732///
733/// # Errors
734///
735/// Returns an error if the validation check fails.
736#[inline(always)]
737pub fn check_key_not_in_map<M>(key: &M::Key, map: &M, key_name: &str, map_name: &str) -> Result<()>
738where
739    M: MapLike,
740{
741    if map.contains_key(key) {
742        return Err(CorrectnessError::KeyPresent {
743            key_name: key_name.to_string(),
744            map_name: map_name.to_string(),
745            key: key.to_string(),
746            map_type_repr: map_type_repr::<M>(),
747        });
748    }
749    Ok(())
750}
751
752/// Checks the `key` is in the `map`.
753///
754/// # Errors
755///
756/// Returns an error if the validation check fails.
757#[inline(always)]
758pub fn check_key_in_map<M>(key: &M::Key, map: &M, key_name: &str, map_name: &str) -> Result<()>
759where
760    M: MapLike,
761{
762    if !map.contains_key(key) {
763        return Err(CorrectnessError::KeyMissing {
764            key_name: key_name.to_string(),
765            map_name: map_name.to_string(),
766            key: key.to_string(),
767            map_type_repr: map_type_repr::<M>(),
768        });
769    }
770    Ok(())
771}
772
773/// Checks the `member` is **not** in the `set`.
774///
775/// # Errors
776///
777/// Returns an error if the validation check fails.
778#[inline(always)]
779pub fn check_member_not_in_set<S>(
780    member: &S::Item,
781    set: &S,
782    member_name: &str,
783    set_name: &str,
784) -> Result<()>
785where
786    S: SetLike,
787{
788    if set.contains(member) {
789        return Err(CorrectnessError::MemberPresent {
790            member_name: member_name.to_string(),
791            set_name: set_name.to_string(),
792            set_type_repr: set_type_repr::<S>(),
793        });
794    }
795    Ok(())
796}
797
798/// Checks the `member` is in the `set`.
799///
800/// # Errors
801///
802/// Returns an error if the validation check fails.
803#[inline(always)]
804pub fn check_member_in_set<S>(
805    member: &S::Item,
806    set: &S,
807    member_name: &str,
808    set_name: &str,
809) -> Result<()>
810where
811    S: SetLike,
812{
813    if !set.contains(member) {
814        return Err(CorrectnessError::MemberMissing {
815            member_name: member_name.to_string(),
816            set_name: set_name.to_string(),
817            set_type_repr: set_type_repr::<S>(),
818        });
819    }
820    Ok(())
821}
822
823/// Checks the `Decimal` value is positive (> 0).
824///
825/// # Errors
826///
827/// Returns an error if the validation check fails.
828#[inline(always)]
829pub fn check_positive_decimal(value: Decimal, param: &str) -> Result<()> {
830    if value <= Decimal::ZERO {
831        return Err(CorrectnessError::NotPositive {
832            param: param.to_string(),
833            value: value.to_string(),
834            type_name: "Decimal",
835        });
836    }
837    Ok(())
838}
839
840fn slice_type_repr<T>() -> String {
841    format!("&[{}]", std::any::type_name::<T>())
842}
843
844fn map_type_repr<M>() -> String
845where
846    M: MapLike,
847{
848    format!(
849        "&<{}, {}>",
850        std::any::type_name::<M::Key>(),
851        std::any::type_name::<M::Value>(),
852    )
853}
854
855fn set_type_repr<S>() -> String
856where
857    S: SetLike,
858{
859    format!("&<{}>", std::any::type_name::<S::Item>())
860}
861
862#[cfg(test)]
863mod tests {
864    use std::{
865        collections::{HashMap, HashSet},
866        fmt::Display,
867        str::FromStr,
868    };
869
870    use rstest::rstest;
871    use rust_decimal::Decimal;
872
873    use super::*;
874
875    #[rstest]
876    fn test_check_predicate_true_returns_typed_error_with_stable_display() {
877        let error = check_predicate_true(false, "the predicate was false").unwrap_err();
878
879        assert_eq!(
880            error,
881            CorrectnessError::PredicateViolation {
882                message: "the predicate was false".to_string(),
883            }
884        );
885        assert_eq!(error.to_string(), "the predicate was false");
886    }
887
888    #[rstest]
889    fn test_expect_display_returns_ok_value() {
890        let result: CorrectnessResult<i32> = Ok(42);
891        assert_eq!(result.expect_display(FAILED), 42);
892    }
893
894    #[rstest]
895    #[should_panic(expected = "Condition failed: invalid string for 'value', was empty")]
896    fn test_expect_display_panics_with_display_form_on_err() {
897        let result: CorrectnessResult<()> = Err(CorrectnessError::EmptyString {
898            param: "value".to_string(),
899        });
900        result.expect_display(FAILED);
901    }
902
903    #[rstest]
904    #[should_panic(expected = "custom prefix: the predicate was false")]
905    fn test_expect_display_uses_provided_prefix() {
906        let result: CorrectnessResult<()> = Err(CorrectnessError::PredicateViolation {
907            message: "the predicate was false".to_string(),
908        });
909        result.expect_display("custom prefix");
910    }
911
912    #[rstest]
913    #[case(false, false)]
914    #[case(true, true)]
915    fn test_check_predicate_true(#[case] predicate: bool, #[case] expected: bool) {
916        let result = check_predicate_true(predicate, "the predicate was false").is_ok();
917        assert_eq!(result, expected);
918    }
919
920    #[rstest]
921    #[case(false, true)]
922    #[case(true, false)]
923    fn test_check_predicate_false(#[case] predicate: bool, #[case] expected: bool) {
924        let result = check_predicate_false(predicate, "the predicate was true").is_ok();
925        assert_eq!(result, expected);
926    }
927
928    #[rstest]
929    #[case("a")]
930    #[case(" ")] // <-- whitespace is allowed
931    #[case("  ")] // <-- multiple whitespace is allowed
932    #[case("🦀")] // <-- non-ASCII is allowed
933    #[case(" a")]
934    #[case("a ")]
935    #[case("abc")]
936    fn test_check_nonempty_string_with_valid_values(#[case] s: &str) {
937        assert!(check_nonempty_string(s, "value").is_ok());
938    }
939
940    #[rstest]
941    #[case("")] // empty string
942    fn test_check_nonempty_string_with_invalid_values(#[case] s: &str) {
943        assert!(check_nonempty_string(s, "value").is_err());
944    }
945
946    #[rstest]
947    #[case(" a")]
948    #[case("a ")]
949    #[case("a a")]
950    #[case(" a ")]
951    #[case("abc")]
952    fn test_check_valid_string_ascii_with_valid_value(#[case] s: &str) {
953        assert!(check_valid_string_ascii(s, "value").is_ok());
954    }
955
956    #[rstest]
957    #[case("")] // <-- empty string
958    #[case(" ")] // <-- whitespace-only
959    #[case("  ")] // <-- whitespace-only string
960    #[case("🦀")] // <-- contains non-ASCII char
961    fn test_check_valid_string_ascii_with_invalid_values(#[case] s: &str) {
962        assert!(check_valid_string_ascii(s, "value").is_err());
963    }
964
965    #[rstest]
966    fn test_check_valid_string_ascii_returns_empty_string_error_with_stable_display() {
967        let error = check_valid_string_ascii("", "value").unwrap_err();
968
969        assert_eq!(
970            error,
971            CorrectnessError::EmptyString {
972                param: "value".to_string(),
973            }
974        );
975        assert_eq!(error.to_string(), "invalid string for 'value', was empty");
976    }
977
978    #[rstest]
979    fn test_check_valid_string_ascii_returns_non_ascii_error_with_stable_display() {
980        let error = check_valid_string_ascii("🦀", "value").unwrap_err();
981
982        assert_eq!(
983            error,
984            CorrectnessError::NonAsciiString {
985                param: "value".to_string(),
986                value: "🦀".to_string(),
987            }
988        );
989        assert_eq!(
990            error.to_string(),
991            "invalid string for 'value' contained a non-ASCII char, was '🦀'"
992        );
993    }
994
995    #[rstest]
996    fn test_check_valid_string_ascii_returns_whitespace_string_error_with_stable_display() {
997        let error = check_valid_string_ascii("   ", "value").unwrap_err();
998
999        assert_eq!(
1000            error,
1001            CorrectnessError::WhitespaceString {
1002                param: "value".to_string(),
1003            }
1004        );
1005        assert_eq!(
1006            error.to_string(),
1007            "invalid string for 'value', was all whitespace"
1008        );
1009    }
1010
1011    #[rstest]
1012    #[case(" a")]
1013    #[case("a ")]
1014    #[case("abc")]
1015    #[case("ETHUSDT")]
1016    fn test_check_valid_string_utf8_with_valid_values(#[case] s: &str) {
1017        assert!(check_valid_string_utf8(s, "value").is_ok());
1018    }
1019
1020    #[rstest]
1021    #[case("")] // <-- empty string
1022    #[case(" ")] // <-- whitespace-only
1023    #[case("  ")] // <-- whitespace-only string
1024    fn test_check_valid_string_utf8_with_invalid_values(#[case] s: &str) {
1025        assert!(check_valid_string_utf8(s, "value").is_err());
1026    }
1027
1028    #[rstest]
1029    #[case(None)]
1030    #[case(Some(" a"))]
1031    #[case(Some("a "))]
1032    #[case(Some("a a"))]
1033    #[case(Some(" a "))]
1034    #[case(Some("abc"))]
1035    fn test_check_valid_string_ascii_optional_with_valid_value(#[case] s: Option<&str>) {
1036        assert!(check_valid_string_ascii_optional(s, "value").is_ok());
1037    }
1038
1039    #[rstest]
1040    #[case("a", "a")]
1041    fn test_check_string_contains_when_does_contain(#[case] s: &str, #[case] pat: &str) {
1042        assert!(check_string_contains(s, pat, "value").is_ok());
1043    }
1044
1045    #[rstest]
1046    #[case("a", "b")]
1047    fn test_check_string_contains_when_does_not_contain(#[case] s: &str, #[case] pat: &str) {
1048        assert!(check_string_contains(s, pat, "value").is_err());
1049    }
1050
1051    #[rstest]
1052    #[case(0u8, 0u8, "left", "right", true)]
1053    #[case(1u8, 1u8, "left", "right", true)]
1054    #[case(0u8, 1u8, "left", "right", false)]
1055    #[case(1u8, 0u8, "left", "right", false)]
1056    #[case(10i32, 10i32, "left", "right", true)]
1057    #[case(10i32, 20i32, "left", "right", false)]
1058    #[case("hello", "hello", "left", "right", true)]
1059    #[case("hello", "world", "left", "right", false)]
1060    fn test_check_equal<T: PartialEq + Debug + Display>(
1061        #[case] lhs: T,
1062        #[case] rhs: T,
1063        #[case] lhs_param: &str,
1064        #[case] rhs_param: &str,
1065        #[case] expected: bool,
1066    ) {
1067        let result = check_equal(&lhs, &rhs, lhs_param, rhs_param).is_ok();
1068        assert_eq!(result, expected);
1069    }
1070
1071    #[rstest]
1072    #[case(0, 0, "left", "right", true)]
1073    #[case(1, 1, "left", "right", true)]
1074    #[case(0, 1, "left", "right", false)]
1075    #[case(1, 0, "left", "right", false)]
1076    fn test_check_equal_u8_when_equal(
1077        #[case] lhs: u8,
1078        #[case] rhs: u8,
1079        #[case] lhs_param: &str,
1080        #[case] rhs_param: &str,
1081        #[case] expected: bool,
1082    ) {
1083        let result = check_equal_u8(lhs, rhs, lhs_param, rhs_param).is_ok();
1084        assert_eq!(result, expected);
1085    }
1086
1087    #[rstest]
1088    fn test_check_equal_u8_returns_equality_mismatch_with_stable_display() {
1089        let error = check_equal_u8(1, 2, "left", "right").unwrap_err();
1090
1091        assert_eq!(
1092            error,
1093            CorrectnessError::EqualityMismatch {
1094                lhs_param: "left".to_string(),
1095                rhs_param: "right".to_string(),
1096                lhs: "1".to_string(),
1097                rhs: "2".to_string(),
1098                type_name: "u8",
1099            }
1100        );
1101        assert_eq!(
1102            error.to_string(),
1103            "'left' u8 of 1 was not equal to 'right' u8 of 2"
1104        );
1105    }
1106
1107    #[rstest]
1108    #[case(0, 0, "left", "right", true)]
1109    #[case(1, 1, "left", "right", true)]
1110    #[case(0, 1, "left", "right", false)]
1111    #[case(1, 0, "left", "right", false)]
1112    fn test_check_equal_usize_when_equal(
1113        #[case] lhs: usize,
1114        #[case] rhs: usize,
1115        #[case] lhs_param: &str,
1116        #[case] rhs_param: &str,
1117        #[case] expected: bool,
1118    ) {
1119        let result = check_equal_usize(lhs, rhs, lhs_param, rhs_param).is_ok();
1120        assert_eq!(result, expected);
1121    }
1122
1123    #[rstest]
1124    #[case(1, "value")]
1125    fn test_check_positive_u64_when_positive(#[case] value: u64, #[case] param: &str) {
1126        assert!(check_positive_u64(value, param).is_ok());
1127    }
1128
1129    #[rstest]
1130    #[case(0, "value")]
1131    fn test_check_positive_u64_when_not_positive(#[case] value: u64, #[case] param: &str) {
1132        assert!(check_positive_u64(value, param).is_err());
1133    }
1134
1135    #[rstest]
1136    #[case(1, "value")]
1137    fn test_check_positive_i64_when_positive(#[case] value: i64, #[case] param: &str) {
1138        assert!(check_positive_i64(value, param).is_ok());
1139    }
1140
1141    #[rstest]
1142    #[case(0, "value")]
1143    #[case(-1, "value")]
1144    fn test_check_positive_i64_when_not_positive(#[case] value: i64, #[case] param: &str) {
1145        assert!(check_positive_i64(value, param).is_err());
1146    }
1147
1148    #[rstest]
1149    #[case(0.0, "value")]
1150    #[case(1.0, "value")]
1151    fn test_check_non_negative_f64_when_not_negative(#[case] value: f64, #[case] param: &str) {
1152        assert!(check_non_negative_f64(value, param).is_ok());
1153    }
1154
1155    #[rstest]
1156    #[case(f64::NAN, "value")]
1157    #[case(f64::INFINITY, "value")]
1158    #[case(f64::NEG_INFINITY, "value")]
1159    #[case(-0.1, "value")]
1160    fn test_check_non_negative_f64_when_negative(#[case] value: f64, #[case] param: &str) {
1161        assert!(check_non_negative_f64(value, param).is_err());
1162    }
1163
1164    #[rstest]
1165    #[case(0, 0, 0, "value")]
1166    #[case(0, 0, 1, "value")]
1167    #[case(1, 0, 1, "value")]
1168    fn test_check_in_range_inclusive_u8_when_in_range(
1169        #[case] value: u8,
1170        #[case] l: u8,
1171        #[case] r: u8,
1172        #[case] desc: &str,
1173    ) {
1174        assert!(check_in_range_inclusive_u8(value, l, r, desc).is_ok());
1175    }
1176
1177    #[rstest]
1178    #[case(0, 1, 2, "value")]
1179    #[case(3, 1, 2, "value")]
1180    fn test_check_in_range_inclusive_u8_when_out_of_range(
1181        #[case] value: u8,
1182        #[case] l: u8,
1183        #[case] r: u8,
1184        #[case] param: &str,
1185    ) {
1186        assert!(check_in_range_inclusive_u8(value, l, r, param).is_err());
1187    }
1188
1189    #[rstest]
1190    #[case(0, 0, 0, "value")]
1191    #[case(0, 0, 1, "value")]
1192    #[case(1, 0, 1, "value")]
1193    fn test_check_in_range_inclusive_u64_when_in_range(
1194        #[case] value: u64,
1195        #[case] l: u64,
1196        #[case] r: u64,
1197        #[case] param: &str,
1198    ) {
1199        assert!(check_in_range_inclusive_u64(value, l, r, param).is_ok());
1200    }
1201
1202    #[rstest]
1203    #[case(0, 1, 2, "value")]
1204    #[case(3, 1, 2, "value")]
1205    fn test_check_in_range_inclusive_u64_when_out_of_range(
1206        #[case] value: u64,
1207        #[case] l: u64,
1208        #[case] r: u64,
1209        #[case] param: &str,
1210    ) {
1211        assert!(check_in_range_inclusive_u64(value, l, r, param).is_err());
1212    }
1213
1214    #[rstest]
1215    #[case(0, 0, 0, "value")]
1216    #[case(0, 0, 1, "value")]
1217    #[case(1, 0, 1, "value")]
1218    fn test_check_in_range_inclusive_i64_when_in_range(
1219        #[case] value: i64,
1220        #[case] l: i64,
1221        #[case] r: i64,
1222        #[case] param: &str,
1223    ) {
1224        assert!(check_in_range_inclusive_i64(value, l, r, param).is_ok());
1225    }
1226
1227    #[rstest]
1228    #[case(0.0, 0.0, 0.0, "value")]
1229    #[case(0.0, 0.0, 1.0, "value")]
1230    #[case(1.0, 0.0, 1.0, "value")]
1231    fn test_check_in_range_inclusive_f64_when_in_range(
1232        #[case] value: f64,
1233        #[case] l: f64,
1234        #[case] r: f64,
1235        #[case] param: &str,
1236    ) {
1237        assert!(check_in_range_inclusive_f64(value, l, r, param).is_ok());
1238    }
1239
1240    #[rstest]
1241    #[case(-1e16, 0.0, 0.0, "value")]
1242    #[case(1.0 + 1e16, 0.0, 1.0, "value")]
1243    fn test_check_in_range_inclusive_f64_when_out_of_range(
1244        #[case] value: f64,
1245        #[case] l: f64,
1246        #[case] r: f64,
1247        #[case] param: &str,
1248    ) {
1249        assert!(check_in_range_inclusive_f64(value, l, r, param).is_err());
1250    }
1251
1252    #[rstest]
1253    #[case(0, 1, 2, "value")]
1254    #[case(3, 1, 2, "value")]
1255    fn test_check_in_range_inclusive_i64_when_out_of_range(
1256        #[case] value: i64,
1257        #[case] l: i64,
1258        #[case] r: i64,
1259        #[case] param: &str,
1260    ) {
1261        assert!(check_in_range_inclusive_i64(value, l, r, param).is_err());
1262    }
1263
1264    #[rstest]
1265    #[case(0, 0, 0, "value")]
1266    #[case(0, 0, 1, "value")]
1267    #[case(1, 0, 1, "value")]
1268    fn test_check_in_range_inclusive_usize_when_in_range(
1269        #[case] value: usize,
1270        #[case] l: usize,
1271        #[case] r: usize,
1272        #[case] param: &str,
1273    ) {
1274        assert!(check_in_range_inclusive_usize(value, l, r, param).is_ok());
1275    }
1276
1277    #[rstest]
1278    #[case(0, 1, 2, "value")]
1279    #[case(3, 1, 2, "value")]
1280    fn test_check_in_range_inclusive_usize_when_out_of_range(
1281        #[case] value: usize,
1282        #[case] l: usize,
1283        #[case] r: usize,
1284        #[case] param: &str,
1285    ) {
1286        assert!(check_in_range_inclusive_usize(value, l, r, param).is_err());
1287    }
1288
1289    #[rstest]
1290    fn test_check_in_range_inclusive_usize_returns_out_of_range_error_with_stable_display() {
1291        let error = check_in_range_inclusive_usize(3, 1, 2, "value").unwrap_err();
1292
1293        assert_eq!(
1294            error,
1295            CorrectnessError::OutOfRange {
1296                param: "value".to_string(),
1297                min: "1".to_string(),
1298                max: "2".to_string(),
1299                value: "3".to_string(),
1300                type_name: "usize",
1301            }
1302        );
1303        assert_eq!(
1304            error.to_string(),
1305            "invalid usize for 'value' not in range [1, 2], was 3"
1306        );
1307    }
1308
1309    #[rstest]
1310    #[case(vec![], true)]
1311    #[case(vec![1_u8], false)]
1312    fn test_check_slice_empty(#[case] collection: Vec<u8>, #[case] expected: bool) {
1313        let result = check_slice_empty(collection.as_slice(), "param").is_ok();
1314        assert_eq!(result, expected);
1315    }
1316
1317    #[rstest]
1318    #[case(vec![], false)]
1319    #[case(vec![1_u8], true)]
1320    fn test_check_slice_not_empty(#[case] collection: Vec<u8>, #[case] expected: bool) {
1321        let result = check_slice_not_empty(collection.as_slice(), "param").is_ok();
1322        assert_eq!(result, expected);
1323    }
1324
1325    #[rstest]
1326    fn test_check_slice_not_empty_returns_collection_empty_error_with_stable_display() {
1327        let error = check_slice_not_empty::<u8>(&[], "param").unwrap_err();
1328
1329        assert_eq!(
1330            error,
1331            CorrectnessError::CollectionEmpty {
1332                param: "param".to_string(),
1333                collection_kind: "slice",
1334                type_repr: "&[u8]".to_string(),
1335            }
1336        );
1337        assert_eq!(error.to_string(), "the 'param' slice `&[u8]` was empty");
1338    }
1339
1340    #[rstest]
1341    #[case(HashMap::new(), true)]
1342    #[case(HashMap::from([("A".to_string(), 1_u8)]), false)]
1343    fn test_check_map_empty(#[case] map: HashMap<String, u8>, #[case] expected: bool) {
1344        let result = check_map_empty(&map, "param").is_ok();
1345        assert_eq!(result, expected);
1346    }
1347
1348    #[rstest]
1349    #[case(HashMap::new(), false)]
1350    #[case(HashMap::from([("A".to_string(), 1_u8)]), true)]
1351    fn test_check_map_not_empty(#[case] map: HashMap<String, u8>, #[case] expected: bool) {
1352        let result = check_map_not_empty(&map, "param").is_ok();
1353        assert_eq!(result, expected);
1354    }
1355
1356    #[rstest]
1357    #[case(&HashMap::<u32, u32>::new(), 5, "key", "map", true)] // empty map
1358    #[case(&HashMap::from([(1, 10), (2, 20)]), 1, "key", "map", false)] // key exists
1359    #[case(&HashMap::from([(1, 10), (2, 20)]), 5, "key", "map", true)] // key doesn't exist
1360    fn test_check_key_not_in_map(
1361        #[case] map: &HashMap<u32, u32>,
1362        #[case] key: u32,
1363        #[case] key_name: &str,
1364        #[case] map_name: &str,
1365        #[case] expected: bool,
1366    ) {
1367        let result = check_key_not_in_map(&key, map, key_name, map_name).is_ok();
1368        assert_eq!(result, expected);
1369    }
1370
1371    #[rstest]
1372    #[case(&HashMap::<u32, u32>::new(), 5, "key", "map", false)] // empty map
1373    #[case(&HashMap::from([(1, 10), (2, 20)]), 1, "key", "map", true)] // key exists
1374    #[case(&HashMap::from([(1, 10), (2, 20)]), 5, "key", "map", false)] // key doesn't exist
1375    fn test_check_key_in_map(
1376        #[case] map: &HashMap<u32, u32>,
1377        #[case] key: u32,
1378        #[case] key_name: &str,
1379        #[case] map_name: &str,
1380        #[case] expected: bool,
1381    ) {
1382        let result = check_key_in_map(&key, map, key_name, map_name).is_ok();
1383        assert_eq!(result, expected);
1384    }
1385
1386    #[rstest]
1387    fn test_check_key_in_map_returns_key_missing_error_with_stable_display() {
1388        let map = HashMap::<u32, u32>::new();
1389        let error = check_key_in_map(&5, &map, "key", "map").unwrap_err();
1390
1391        assert_eq!(
1392            error,
1393            CorrectnessError::KeyMissing {
1394                key_name: "key".to_string(),
1395                map_name: "map".to_string(),
1396                key: "5".to_string(),
1397                map_type_repr: "&<u32, u32>".to_string(),
1398            }
1399        );
1400        assert_eq!(
1401            error.to_string(),
1402            "the 'key' key 5 was not in the 'map' map `&<u32, u32>`"
1403        );
1404    }
1405
1406    #[rstest]
1407    #[case(&HashSet::<u32>::new(), 5, "member", "set", true)] // Empty set
1408    #[case(&HashSet::from([1, 2]), 1, "member", "set", false)] // Member exists
1409    #[case(&HashSet::from([1, 2]), 5, "member", "set", true)] // Member doesn't exist
1410    fn test_check_member_not_in_set(
1411        #[case] set: &HashSet<u32>,
1412        #[case] member: u32,
1413        #[case] member_name: &str,
1414        #[case] set_name: &str,
1415        #[case] expected: bool,
1416    ) {
1417        let result = check_member_not_in_set(&member, set, member_name, set_name).is_ok();
1418        assert_eq!(result, expected);
1419    }
1420
1421    #[rstest]
1422    #[case(&HashSet::<u32>::new(), 5, "member", "set", false)] // Empty set
1423    #[case(&HashSet::from([1, 2]), 1, "member", "set", true)] // Member exists
1424    #[case(&HashSet::from([1, 2]), 5, "member", "set", false)] // Member doesn't exist
1425    fn test_check_member_in_set(
1426        #[case] set: &HashSet<u32>,
1427        #[case] member: u32,
1428        #[case] member_name: &str,
1429        #[case] set_name: &str,
1430        #[case] expected: bool,
1431    ) {
1432        let result = check_member_in_set(&member, set, member_name, set_name).is_ok();
1433        assert_eq!(result, expected);
1434    }
1435
1436    #[rstest]
1437    #[case("1", true)] // simple positive integer
1438    #[case("0.0000000000000000000000000001", true)] // smallest positive (1 × 10⁻²⁸)
1439    #[case("79228162514264337593543950335", true)] // very large positive (≈ Decimal::MAX)
1440    #[case("0", false)] // zero should fail
1441    #[case("-0.0000000000000000000000000001", false)] // tiny negative
1442    #[case("-1", false)] // simple negative integer
1443    fn test_check_positive_decimal(#[case] raw: &str, #[case] expected: bool) {
1444        let value = Decimal::from_str(raw).expect("valid decimal literal");
1445        let result = super::check_positive_decimal(value, "param").is_ok();
1446        assert_eq!(result, expected);
1447    }
1448
1449    #[rstest]
1450    #[case(1, true)]
1451    #[case(u128::MAX, true)]
1452    #[case(0, false)]
1453    fn test_check_positive_u128(#[case] value: u128, #[case] expected: bool) {
1454        assert_eq!(check_positive_u128(value, "value").is_ok(), expected);
1455    }
1456
1457    #[rstest]
1458    #[case(1, true)]
1459    #[case(i128::MAX, true)]
1460    #[case(0, false)]
1461    #[case(-1, false)]
1462    #[case(i128::MIN, false)]
1463    fn test_check_positive_i128(#[case] value: i128, #[case] expected: bool) {
1464        assert_eq!(check_positive_i128(value, "value").is_ok(), expected);
1465    }
1466
1467    #[rstest]
1468    fn test_check_positive_decimal_returns_not_positive_error_with_stable_display() {
1469        let error = check_positive_decimal(Decimal::ZERO, "param").unwrap_err();
1470
1471        assert_eq!(
1472            error,
1473            CorrectnessError::NotPositive {
1474                param: "param".to_string(),
1475                value: "0".to_string(),
1476                type_name: "Decimal",
1477            }
1478        );
1479        assert_eq!(
1480            error.to_string(),
1481            "invalid Decimal for 'param' not positive, was 0"
1482        );
1483    }
1484}