xsd_types/lexical/decimal/
mod.rs

1use super::lexical_form;
2use std::borrow::{Borrow, ToOwned};
3use std::cmp::Ordering;
4use std::fmt;
5use std::hash::{Hash, Hasher};
6
7/// Numeric sign.
8#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)]
9pub enum Sign {
10	Negative,
11	Zero,
12	Positive,
13}
14
15impl Sign {
16	pub fn is_positive(&self) -> bool {
17		matches!(self, Self::Positive)
18	}
19
20	pub fn is_negative(&self) -> bool {
21		matches!(self, Self::Negative)
22	}
23
24	pub fn is_zero(&self) -> bool {
25		matches!(self, Self::Zero)
26	}
27}
28
29/// Error thrown when a conversion function overflowed.
30pub struct Overflow;
31
32mod integer;
33
34pub use integer::*;
35
36lexical_form! {
37	/// Decimal number.
38	///
39	/// See: <https://www.w3.org/TR/xmlschema-2/#decimal>
40	ty: Decimal,
41
42	/// Owned decimal number.
43	///
44	/// See: <https://www.w3.org/TR/xmlschema-2/#decimal>
45	buffer: DecimalBuf,
46
47	/// Creates a new decimal from a string.
48	///
49	/// If the input string is ot a [valid XSD decimal](https://www.w3.org/TR/xmlschema-2/#decimal),
50	/// an [`InvalidDecimal`] error is returned.
51	new,
52
53	/// Creates a new decimal from a string without checking it.
54	///
55	/// # Safety
56	///
57	/// The input string must be a [valid XSD decimal](https://www.w3.org/TR/xmlschema-2/#decimal).
58	new_unchecked,
59
60	value: crate::Decimal,
61	error: InvalidDecimal,
62	as_ref: as_decimal,
63	parent_forms: {}
64}
65
66impl Decimal {
67	/// Returns `true` if `self` is positive
68	/// and `false` is the number is zero or negative.
69	pub fn is_positive(&self) -> bool {
70		let mut sign_positive = true;
71		for c in &self.0 {
72			match c {
73				b'+' | b'0' | b'.' => (),
74				b'-' => sign_positive = false,
75				_ => return sign_positive,
76			}
77		}
78
79		false
80	}
81
82	/// Returns `true` if `self` is negative
83	/// and `false` is the number is zero or positive.
84	pub fn is_negative(&self) -> bool {
85		let mut sign_negative = true;
86		for c in &self.0 {
87			match c {
88				b'-' | b'0' | b'.' => (),
89				b'+' => sign_negative = false,
90				_ => return sign_negative,
91			}
92		}
93
94		false
95	}
96
97	/// Returns `true` if `self` is zero
98	/// and `false` otherwise.
99	pub fn is_zero(&self) -> bool {
100		for c in &self.0 {
101			if !matches!(c, b'+' | b'-' | b'0' | b'.') {
102				return false;
103			}
104		}
105
106		true
107	}
108
109	pub fn sign(&self) -> Sign {
110		let mut sign_positive = true;
111		for c in &self.0 {
112			match c {
113				b'+' | b'0' | b'.' => (),
114				b'-' => sign_positive = false,
115				_ => {
116					if sign_positive {
117						return Sign::Positive;
118					} else {
119						return Sign::Negative;
120					}
121				}
122			}
123		}
124
125		Sign::Zero
126	}
127
128	#[inline(always)]
129	pub fn integer_part(&self) -> &Integer {
130		match self.split_once('.') {
131			Some((integer_part, _)) => unsafe { Integer::new_unchecked(integer_part) },
132			None => unsafe { Integer::new_unchecked(self) },
133		}
134	}
135
136	#[inline(always)]
137	pub fn fractional_part(&self) -> Option<&FractionalPart> {
138		self.split_once('.')
139			.map(|(_, fractional_part)| unsafe { FractionalPart::new_unchecked(fractional_part) })
140	}
141
142	#[inline(always)]
143	pub fn trimmed_fractional_part(&self) -> Option<&FractionalPart> {
144		self.split_once('.').and_then(|(_, fractional_part)| {
145			let f = unsafe { FractionalPart::new_unchecked(fractional_part) }.trimmed();
146			if f.is_empty() {
147				None
148			} else {
149				Some(f)
150			}
151		})
152	}
153
154	#[inline(always)]
155	pub fn parts(&self) -> (&Integer, Option<&FractionalPart>) {
156		match self.split_once('.') {
157			Some((i, f)) => unsafe {
158				(
159					Integer::new_unchecked(i),
160					Some(FractionalPart::new_unchecked(f)),
161				)
162			},
163			None => unsafe { (Integer::new_unchecked(self), None) },
164		}
165	}
166
167	pub fn value(&self) -> crate::Decimal {
168		self.to_owned().into()
169	}
170}
171
172impl PartialEq for Decimal {
173	fn eq(&self, other: &Self) -> bool {
174		self.integer_part() == other.integer_part()
175			&& self.fractional_part() == other.fractional_part()
176	}
177}
178
179impl Eq for Decimal {}
180
181impl Hash for Decimal {
182	fn hash<H: Hasher>(&self, h: &mut H) {
183		self.integer_part().hash(h);
184		match self.fractional_part() {
185			Some(f) => f.hash(h),
186			None => FractionalPart::empty().hash(h),
187		}
188	}
189}
190
191impl Ord for Decimal {
192	fn cmp(&self, other: &Self) -> Ordering {
193		let sign = self.sign();
194		match sign.cmp(&other.sign()) {
195			Ordering::Equal => {
196				let (integer_part, fractional_part) = self.parts();
197				let (other_integer_part, other_fractional_part) = other.parts();
198				match integer_part.cmp(other_integer_part) {
199					Ordering::Equal => {
200						let fractional_part = fractional_part.unwrap_or_else(FractionalPart::empty);
201						let other_fractional_part =
202							other_fractional_part.unwrap_or_else(FractionalPart::empty);
203						if sign.is_negative() {
204							fractional_part.cmp(other_fractional_part).reverse()
205						} else {
206							fractional_part.cmp(other_fractional_part)
207						}
208					}
209					other => {
210						if sign.is_negative() {
211							other.reverse()
212						} else {
213							other
214						}
215					}
216				}
217			}
218			other => other,
219		}
220	}
221}
222
223impl PartialOrd for Decimal {
224	fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
225		Some(self.cmp(other))
226	}
227}
228
229macro_rules! integer_conversion {
230	{ $($ty:ty),* } => {
231		$(
232			impl From<$ty> for DecimalBuf {
233				fn from(i: $ty) -> Self {
234					unsafe { DecimalBuf::new_unchecked(i.to_string()) }
235				}
236			}
237
238			impl<'a> TryFrom<&'a Decimal> for $ty {
239				type Error = Overflow;
240
241				fn try_from(i: &'a Decimal) -> Result<Self, Overflow> {
242					i.as_str().parse().map_err(|_| Overflow)
243				}
244			}
245
246			impl TryFrom<DecimalBuf> for $ty {
247				type Error = Overflow;
248
249				fn try_from(i: DecimalBuf) -> Result<Self, Overflow> {
250					i.as_str().parse().map_err(|_| Overflow)
251				}
252			}
253		)*
254	};
255}
256
257integer_conversion! {
258	u8,
259	i8,
260	u16,
261	i16,
262	u32,
263	i32,
264	u64,
265	i64,
266	usize,
267	isize
268}
269
270const DTOA_CONFIG: pretty_dtoa::FmtFloatConfig =
271	pretty_dtoa::FmtFloatConfig::default().force_no_e_notation();
272
273impl From<f32> for DecimalBuf {
274	fn from(i: f32) -> Self {
275		unsafe { DecimalBuf::new_unchecked(pretty_dtoa::ftoa(i, DTOA_CONFIG)) }
276	}
277}
278
279impl<'a> TryFrom<&'a Decimal> for f32 {
280	type Error = <f32 as std::str::FromStr>::Err;
281
282	fn try_from(i: &'a Decimal) -> Result<Self, Self::Error> {
283		i.as_str().parse()
284	}
285}
286
287impl TryFrom<DecimalBuf> for f32 {
288	type Error = <f32 as std::str::FromStr>::Err;
289
290	fn try_from(i: DecimalBuf) -> Result<Self, Self::Error> {
291		i.as_str().parse()
292	}
293}
294
295impl From<f64> for DecimalBuf {
296	fn from(i: f64) -> Self {
297		unsafe { DecimalBuf::new_unchecked(pretty_dtoa::dtoa(i, DTOA_CONFIG)) }
298	}
299}
300
301impl<'a> TryFrom<&'a Decimal> for f64 {
302	type Error = <f64 as std::str::FromStr>::Err;
303
304	fn try_from(i: &'a Decimal) -> Result<Self, Self::Error> {
305		i.as_str().parse()
306	}
307}
308
309impl TryFrom<DecimalBuf> for f64 {
310	type Error = <f64 as std::str::FromStr>::Err;
311
312	fn try_from(i: DecimalBuf) -> Result<Self, Self::Error> {
313		i.as_str().parse()
314	}
315}
316
317pub struct FractionalPart([u8]);
318
319impl FractionalPart {
320	/// Creates a new fractional part from a byte slice.
321	///
322	/// # Safety
323	///
324	/// The input byte slice must be a valid fractional part lexical
325	/// representation.
326	#[inline(always)]
327	pub unsafe fn new_unchecked<S: ?Sized + AsRef<[u8]>>(s: &S) -> &Self {
328		std::mem::transmute(s.as_ref())
329	}
330
331	#[inline(always)]
332	pub fn empty<'a>() -> &'a Self {
333		unsafe { Self::new_unchecked(b"") }
334	}
335
336	#[inline(always)]
337	pub fn as_str(&self) -> &str {
338		unsafe { core::str::from_utf8_unchecked(&self.0) }
339	}
340
341	#[inline(always)]
342	pub fn as_bytes(&self) -> &[u8] {
343		&self.0
344	}
345
346	#[inline(always)]
347	pub fn is_empty(&self) -> bool {
348		self.0.is_empty()
349	}
350
351	/// Returns the fractional part without the trailing zeros.
352	///
353	/// The returned fractional part may be empty.
354	pub fn trimmed(&self) -> &FractionalPart {
355		let mut end = 0;
356		for (i, &c) in self.0.iter().enumerate() {
357			if c != b'0' {
358				end = i + 1
359			}
360		}
361
362		unsafe { Self::new_unchecked(&self.0[0..end]) }
363	}
364}
365
366impl PartialEq for FractionalPart {
367	fn eq(&self, other: &Self) -> bool {
368		self.trimmed().0 == other.trimmed().0
369	}
370}
371
372impl Eq for FractionalPart {}
373
374impl Hash for FractionalPart {
375	fn hash<H: Hasher>(&self, h: &mut H) {
376		self.trimmed().0.hash(h)
377	}
378}
379
380impl Ord for FractionalPart {
381	fn cmp(&self, other: &Self) -> Ordering {
382		self.trimmed().0.cmp(&other.trimmed().0)
383	}
384}
385
386impl PartialOrd for FractionalPart {
387	fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
388		Some(self.cmp(other))
389	}
390}
391impl AsRef<[u8]> for FractionalPart {
392	fn as_ref(&self) -> &[u8] {
393		&self.0
394	}
395}
396
397impl AsRef<str> for FractionalPart {
398	fn as_ref(&self) -> &str {
399		self.as_str()
400	}
401}
402
403fn check_bytes(s: &[u8]) -> bool {
404	check(s.iter().copied())
405}
406
407fn check<C: Iterator<Item = u8>>(mut chars: C) -> bool {
408	enum State {
409		Initial,
410		NonEmptyInteger,
411		Integer,
412		NonEmptyDecimal,
413		Decimal,
414	}
415
416	let mut state = State::Initial;
417
418	loop {
419		state = match state {
420			State::Initial => match chars.next() {
421				Some(b'+') => State::NonEmptyInteger,
422				Some(b'-') => State::NonEmptyInteger,
423				Some(b'.') => State::NonEmptyDecimal,
424				Some(b'0'..=b'9') => State::Integer,
425				_ => break false,
426			},
427			State::NonEmptyInteger => match chars.next() {
428				Some(b'0'..=b'9') => State::Integer,
429				Some(b'.') => State::Decimal,
430				_ => break false,
431			},
432			State::Integer => match chars.next() {
433				Some(b'0'..=b'9') => State::Integer,
434				Some(b'.') => State::Decimal,
435				Some(_) => break false,
436				None => break true,
437			},
438			State::NonEmptyDecimal => match chars.next() {
439				Some(b'0'..=b'9') => State::Decimal,
440				_ => break false,
441			},
442			State::Decimal => match chars.next() {
443				Some(b'0'..=b'9') => State::Decimal,
444				Some(_) => break false,
445				None => break true,
446			},
447		}
448	}
449}
450
451#[cfg(test)]
452mod tests {
453	use super::*;
454
455	#[test]
456	fn parse_01() {
457		Decimal::new("0").unwrap();
458	}
459
460	#[test]
461	#[should_panic]
462	fn parse_02() {
463		Decimal::new("+").unwrap();
464	}
465
466	#[test]
467	#[should_panic]
468	fn parse_03() {
469		Decimal::new("-").unwrap();
470	}
471
472	#[test]
473	#[should_panic]
474	fn parse_04() {
475		Decimal::new("012+").unwrap();
476	}
477
478	#[test]
479	fn parse_05() {
480		Decimal::new("+42").unwrap();
481	}
482
483	#[test]
484	fn parse_06() {
485		Decimal::new("-42").unwrap();
486	}
487
488	#[test]
489	#[should_panic]
490	fn parse_07() {
491		Decimal::new(".").unwrap();
492	}
493
494	#[test]
495	fn parse_08() {
496		Decimal::new(".0").unwrap();
497	}
498
499	#[test]
500	fn parse_09() {
501		Decimal::new("0.").unwrap();
502	}
503
504	#[test]
505	fn parse_10() {
506		Decimal::new("42.0").unwrap();
507	}
508
509	#[test]
510	fn format_01() {
511		assert_eq!(DecimalBuf::from(1.0e10f32).to_string(), "10000000000.0")
512	}
513
514	#[test]
515	fn cmp_01() {
516		assert!(Decimal::new("0.123").unwrap() < Decimal::new("1.123").unwrap())
517	}
518
519	#[test]
520	fn cmp_02() {
521		assert!(Decimal::new("0.123").unwrap() < Decimal::new("0.1234").unwrap())
522	}
523
524	#[test]
525	fn cmp_03() {
526		assert!(Decimal::new("0.123").unwrap() > Decimal::new("-0.123").unwrap())
527	}
528
529	#[test]
530	fn cmp_04() {
531		assert!(Decimal::new("-0.123").unwrap() > Decimal::new("-0.1234").unwrap())
532	}
533}