one_based/
lib.rs

1//! Provides OneBased* unsigned int types, which wraps several integers as 1-based index.
2//!
3//! Example:
4//! ```
5//! # use one_based::{OneBasedU32, OneBasedError};
6//! # use std::num::NonZeroU32;
7//! let v: OneBasedU32 = "5".parse().unwrap();
8//! assert_eq!(v.as_zero_based(), 4);
9//! assert_eq!(v.as_one_based(), NonZeroU32::new(5).unwrap());
10//!
11//! let v: Result<OneBasedU32, OneBasedError> = OneBasedU32::from_one_based(0);
12//! assert_eq!(v.unwrap_err(), OneBasedError::ZeroIndex);
13//! ```
14
15use std::{
16    fmt::Display,
17    num::{
18        NonZeroU128, NonZeroU16, NonZeroU32, NonZeroU64, NonZeroU8, NonZeroUsize, ParseIntError,
19    },
20    str::FromStr,
21};
22
23macro_rules! define_one_based {
24    ($name:ident, $itype:ty, $nonzerotype:ty) => {
25        #[doc = concat!(r" Represents 1-based index of ", stringify!($itype), r".")]
26        ///
27        /// To describe configuration by humans, often 1-based index is easier than 0-based to understand.
28        /// On the other hand, 0-based index is easier to use in the programming.
29        /// Also, it's quite hard to track if the index is 0-based or 1-based.
30        /// `$name` provides ergonomics to handle user provided 1-baed index safely.
31        ///
32        #[doc = r" ```"]
33        #[doc = r" # fn main() -> Result<(), one_based::OneBasedError> {"]
34        #[doc = concat!(r" # use one_based::", stringify!($name), r";")]
35        #[doc = r" // Creates from 1-based index"]
36        #[doc = concat!(r" let v = ", stringify!($name),r"::from_one_based(5)?;")]
37        #[doc = r" assert_eq!(v.as_zero_based(), 4);"]
38        #[doc = r""]
39        #[doc = r" // Creates from 0-based index"]
40        #[doc = concat!(r" let v = ", stringify!($name),r"::from_zero_based(0)?;")]
41        #[doc = r" assert_eq!(v.as_one_based().get(), 1);"]
42        #[doc = r" # Ok(())"]
43        #[doc = r" # }"]
44        #[doc = r" ```"]
45        #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
46        #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
47        pub struct $name($nonzerotype);
48
49        impl Display for $name {
50            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51                self.as_one_based().fmt(f)
52            }
53        }
54
55        impl FromStr for $name {
56            type Err = ParseIntError;
57
58            fn from_str(s: &str) -> Result<Self, Self::Err> {
59                let v: $nonzerotype = s.parse()?;
60                Ok(Self::from_one_based_nonzero(v))
61            }
62        }
63
64        impl $name {
65            /// Creates `$name` from 1-based index value.
66            /// Returns error if the given index is zero.
67            pub fn from_one_based(v: $itype) -> Result<Self, OneBasedError> {
68                let v = <$nonzerotype>::new(v).ok_or(OneBasedError::ZeroIndex)?;
69                Ok($name(v))
70            }
71
72            /// Creates `$name` from 1-based index value as [`$nonzerotype`].
73            /// This will always succeed.
74            #[inline]
75            pub fn from_one_based_nonzero(v: $nonzerotype) -> Self {
76                Self(v)
77            }
78
79            /// Creates `$name` from 0-based index value.
80            /// Returns error if the given index is MAX value,
81            /// as that would case overflow when converted to 1-based.
82            pub fn from_zero_based(v: $itype) -> Result<Self, OneBasedError> {
83                if v == <$nonzerotype>::MAX.get() {
84                    return Err(OneBasedError::OverflowIndex);
85                }
86                let v = unsafe {
87                    // this won't overflow, and cannot be zero (note all $itype is unsigned).
88                    <$nonzerotype>::new_unchecked(v + 1)
89                };
90                Ok($name(v))
91            }
92
93            /// Returns regular 0-based index.
94            pub fn as_zero_based(&self) -> $itype {
95                self.0.get() - 1
96            }
97
98            /// Returns 1-based index.
99            pub fn as_one_based(&self) -> $nonzerotype {
100                self.0
101            }
102        }
103    };
104}
105
106define_one_based!(OneBasedU8, u8, NonZeroU8);
107define_one_based!(OneBasedU16, u16, NonZeroU16);
108define_one_based!(OneBasedU32, u32, NonZeroU32);
109define_one_based!(OneBasedU64, u64, NonZeroU64);
110define_one_based!(OneBasedU128, u128, NonZeroU128);
111define_one_based!(OneBasedUsize, usize, NonZeroUsize);
112
113/// Error type used when converting integer to OneBased* types.
114#[derive(Debug, Clone, PartialEq, Eq)]
115pub enum OneBasedError {
116    ZeroIndex,
117    OverflowIndex,
118}
119
120impl Display for OneBasedError {
121    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
122        match self {
123            OneBasedError::ZeroIndex => f.write_str("0 passed as 1-based index"),
124            OneBasedError::OverflowIndex => {
125                f.write_str("unsigned::MAX cannot be used as 0-based index")
126            }
127        }
128    }
129}
130
131impl std::error::Error for OneBasedError {}
132
133#[cfg(test)]
134mod tests {
135    use std::num::IntErrorKind;
136
137    use super::*;
138
139    #[test]
140    fn in_range_ints_converts_each_other() {
141        assert_eq!(
142            OneBasedUsize::from_one_based_nonzero(NonZeroUsize::new(1).unwrap()).as_zero_based(),
143            0
144        );
145        assert_eq!(
146            OneBasedU16::from_zero_based(u16::MAX - 1)
147                .unwrap()
148                .as_one_based(),
149            NonZeroU16::MAX
150        );
151    }
152
153    #[test]
154    fn overflow_fails_on_zero_based() {
155        assert_eq!(
156            Err(OneBasedError::OverflowIndex),
157            OneBasedU8::from_zero_based(u8::MAX)
158        );
159        assert_eq!(
160            Err(OneBasedError::OverflowIndex),
161            OneBasedU16::from_zero_based(u16::MAX)
162        );
163        assert_eq!(
164            Err(OneBasedError::OverflowIndex),
165            OneBasedU32::from_zero_based(u32::MAX)
166        );
167        assert_eq!(
168            Err(OneBasedError::OverflowIndex),
169            OneBasedU64::from_zero_based(u64::MAX)
170        );
171        assert_eq!(
172            Err(OneBasedError::OverflowIndex),
173            OneBasedU128::from_zero_based(u128::MAX)
174        );
175    }
176
177    #[test]
178    fn from_str_and_to_string() {
179        let v: OneBasedU16 = "12345".parse().unwrap();
180        assert_eq!(v.as_zero_based(), 12344u16);
181        assert_eq!(&v.to_string(), "12345");
182    }
183
184    #[test]
185    fn from_str_failures() {
186        let err = OneBasedU8::from_str("0").unwrap_err();
187        assert_eq!(*err.kind(), IntErrorKind::Zero);
188
189        let err = OneBasedU8::from_str("256").unwrap_err();
190        assert_eq!(*err.kind(), IntErrorKind::PosOverflow);
191    }
192}