1use crate::isl::ranges::base::RangeValidation;
19use crate::isl::util::TimestampPrecision;
20use crate::IonSchemaResult;
21use crate::{invalid_schema_error, invalid_schema_error_raw, isl_require};
22use ion_rs::Decimal;
23use ion_rs::{Element, IonResult, ValueWriter, WriteAsIon};
24use ion_rs::{IonType, Timestamp};
25use num_traits::{CheckedAdd, One};
26use std::cmp::Ordering;
27use std::fmt::{Display, Formatter};
28
29#[derive(Debug, PartialEq, Clone)]
31pub enum Limit<T> {
32 Min,
42 Max,
43 Inclusive(T),
45 Exclusive(T),
47}
48
49impl<T: PartialOrd> PartialEq<T> for Limit<T> {
50 fn eq(&self, other: &T) -> bool {
51 match self {
52 Limit::Inclusive(x) => x == other,
53 _ => false,
54 }
55 }
56}
57
58impl<T: PartialOrd> PartialOrd<T> for Limit<T> {
59 fn partial_cmp(&self, other: &T) -> Option<Ordering> {
60 match self {
61 Limit::Min => Some(Ordering::Less),
62 Limit::Max => Some(Ordering::Greater),
63 Limit::Inclusive(x) => x.partial_cmp(other),
64 Limit::Exclusive(x) => {
65 match x.partial_cmp(other) {
67 Some(Ordering::Equal) => None,
68 order => order,
69 }
70 }
71 }
72 }
73}
74
75impl<T: Display> Display for Limit<T> {
76 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
77 match self {
78 Limit::Min => "min".fmt(f),
79 Limit::Max => "max".fmt(f),
80 Limit::Inclusive(value) => value.fmt(f),
81 Limit::Exclusive(value) => write!(f, "exclusive::{value}"),
82 }
83 }
84}
85
86impl<T: WriteAsIon> WriteAsIon for Limit<T> {
87 fn write_as_ion<V: ValueWriter>(&self, writer: V) -> IonResult<()> {
88 match self {
89 Limit::Min => writer.write_symbol("min"),
90 Limit::Max => writer.write_symbol("max"),
91 Limit::Inclusive(t) => writer.write(t),
92 Limit::Exclusive(t) => writer.with_annotations(["exclusive"])?.write(t),
93 }
94 }
95}
96
97pub type UsizeRange = base::Range<usize>;
98impl RangeValidation<usize> for UsizeRange {
99 fn is_empty(start: &Limit<usize>, end: &Limit<usize>) -> bool {
100 base::is_int_range_empty(start, end)
101 }
102}
103impl UsizeRange {
104 pub fn one() -> Self {
106 Self::new_single_value(1)
107 }
108
109 pub fn zero_or_one() -> Self {
111 Self::new_inclusive(0, 1).expect("This is safe to unwrap because 0 <= 1")
112 }
113
114 pub fn inclusive_endpoints(&self) -> (usize, usize) {
115 let lower = match self.lower() {
119 Limit::Min => 0,
120 Limit::Inclusive(x) => *x,
121 Limit::Exclusive(x) => x + 1,
122 Limit::Max => unreachable!(),
123 };
124 let upper = match self.upper() {
125 Limit::Max => usize::MAX,
126 Limit::Inclusive(x) => *x,
127 Limit::Exclusive(x) => x - 1,
128 Limit::Min => unreachable!(),
129 };
130 (lower, upper)
131 }
132}
133
134pub type U64Range = base::Range<u64>;
135impl RangeValidation<u64> for U64Range {
136 fn is_empty(start: &Limit<u64>, end: &Limit<u64>) -> bool {
137 base::is_int_range_empty(start, end)
138 }
139}
140
141pub type I64Range = base::Range<i64>;
142impl RangeValidation<i64> for I64Range {
143 fn is_empty(start: &Limit<i64>, end: &Limit<i64>) -> bool {
144 base::is_int_range_empty(start, end)
145 }
146}
147
148pub type NumberRange = base::Range<Decimal>;
149impl RangeValidation<Decimal> for NumberRange {}
150
151pub type TimestampRange = base::Range<Timestamp>;
152impl RangeValidation<Timestamp> for TimestampRange {}
153
154pub type TimestampPrecisionRange = base::Range<TimestampPrecision>;
155impl RangeValidation<TimestampPrecision> for TimestampPrecisionRange {
156 fn is_empty(start: &Limit<TimestampPrecision>, end: &Limit<TimestampPrecision>) -> bool {
157 match (start, end) {
158 (Limit::Inclusive(lower), Limit::Inclusive(upper)) => lower > upper,
159 (Limit::Exclusive(lower), Limit::Inclusive(upper))
160 | (Limit::Inclusive(lower), Limit::Exclusive(upper)) => lower >= upper,
161 (Limit::Exclusive(lower), Limit::Exclusive(upper)) => {
162 let start_value = lower.int_value();
163 let end_value = upper.int_value();
164
165 let adjusted_lower = start_value + 1;
167 adjusted_lower >= end_value
168 }
169 _ => false,
170 }
171 }
172}
173
174mod base {
176 use super::*;
177
178 pub trait RangeValidation<T: PartialOrd> {
181 fn is_empty(start: &Limit<T>, end: &Limit<T>) -> bool {
183 match (start, end) {
184 (Limit::Inclusive(lower), Limit::Inclusive(upper)) => lower > upper,
185 (Limit::Exclusive(lower), Limit::Exclusive(upper))
186 | (Limit::Exclusive(lower), Limit::Inclusive(upper))
187 | (Limit::Inclusive(lower), Limit::Exclusive(upper)) => lower >= upper,
188 _ => false,
189 }
190 }
191 }
192
193 pub fn is_int_range_empty<T: CheckedAdd + One + PartialOrd>(
197 start: &Limit<T>,
198 end: &Limit<T>,
199 ) -> bool {
200 match (start, end) {
201 (Limit::Inclusive(lower), Limit::Inclusive(upper)) => lower > upper,
202 (Limit::Exclusive(lower), Limit::Inclusive(upper))
203 | (Limit::Inclusive(lower), Limit::Exclusive(upper)) => lower >= upper,
204 (Limit::Exclusive(lower), Limit::Exclusive(upper)) => {
205 let adjusted_lower = lower.checked_add(&T::one());
207 if adjusted_lower.is_none() {
209 return true;
210 }
211 adjusted_lower.unwrap() >= *upper
212 }
213 _ => false,
214 }
215 }
216
217 #[derive(Debug, Clone, PartialEq)]
222 pub struct Range<T> {
223 lower: Limit<T>,
224 upper: Limit<T>,
225 }
226
227 impl<T: PartialOrd + Clone> Range<T>
230 where
231 Self: RangeValidation<T>,
232 {
233 pub fn new(start: Limit<T>, end: Limit<T>) -> IonSchemaResult<Self> {
236 isl_require!(start != Limit::Min || end != Limit::Max => "range may not contain both 'min' and 'max'")?;
237 isl_require!(start != Limit::Max => "range may not be empty (start of range may not be 'max')")?;
238 isl_require!(end != Limit::Min => "range may not be empty (end of range cannot be 'min')")?;
239 isl_require!(!Self::is_empty(&start, &end) => "range may not be empty")?;
240 Ok(Self {
241 lower: start,
242 upper: end,
243 })
244 }
245
246 pub fn new_inclusive(start: T, end: T) -> IonSchemaResult<Self> {
249 Self::new(Limit::Inclusive(start), Limit::Inclusive(end))
250 }
251
252 pub fn new_single_value(value: T) -> Self {
254 Self::new_inclusive(value.clone(), value).unwrap()
256 }
257
258 pub fn lower(&self) -> &Limit<T> {
259 &self.lower
260 }
261
262 pub fn upper(&self) -> &Limit<T> {
263 &self.upper
264 }
265
266 pub fn contains<V: Into<T> + Clone>(&self, value: &V) -> bool {
268 let other = value.clone().into();
269 self.lower <= other && self.upper >= other
270 }
271
272 pub fn from_ion_element<F: Fn(&Element) -> Option<T>>(
274 element: &Element,
275 value_fn: F,
276 ) -> IonSchemaResult<Range<T>> {
277 if element.annotations().contains("range") {
278 isl_require!(element.ion_type() == IonType::List => "range must be a non-null list; found: {element}")?;
279 isl_require!(!element.is_null() => "range must be a non-null list; found: {element}")?;
280 let seq = element.as_sequence().unwrap();
281 isl_require!(seq.len() == 2 => "range must have a lower and upper bound; found: {element}")?;
282
283 let lower_limit = Self::read_range_bound(element, seq.get(0).unwrap(), &value_fn)?;
284 let upper_limit = Self::read_range_bound(element, seq.get(1).unwrap(), &value_fn)?;
285
286 Self::new(lower_limit, upper_limit)
287 } else {
288 let value = value_fn(element);
289 if let Some(value) = value {
290 Ok(Self::new_single_value(value))
291 } else {
292 invalid_schema_error(format!("invalid value for range: {element}"))
293 }
294 }
295 }
296
297 fn read_range_bound<F: Fn(&Element) -> Option<T>>(
298 element: &Element,
299 boundary_element: &Element,
300 value_fn: F,
301 ) -> IonSchemaResult<Limit<T>> {
302 let is_exclusive = boundary_element.annotations().contains("exclusive");
303 isl_require!(boundary_element.annotations().len() == if is_exclusive {1} else {0} => "invalid annotation(s) on range boundary {element}")?;
304
305 match boundary_element.as_symbol().map(|s| s.text()) {
306 Some(Some("min")) => {
307 isl_require!(!is_exclusive => "'min' may not be exclusive")?;
308 Ok(Limit::Min)
309 }
310 Some(Some("max")) => {
311 isl_require!(!is_exclusive => "'max' may not be exclusive")?;
312 Ok(Limit::Max)
313 }
314 _ => {
315 let limit_value: T = value_fn(boundary_element).ok_or_else(|| {
316 invalid_schema_error_raw(format!(
317 "invalid value for range boundary: {element}"
318 ))
319 })?;
320 if is_exclusive {
321 Ok(Limit::Exclusive(limit_value))
322 } else {
323 Ok(Limit::Inclusive(limit_value))
324 }
325 }
326 }
327 }
328 }
329
330 impl<T: Clone + PartialEq + PartialOrd> From<T> for Range<T>
331 where
332 Self: RangeValidation<T>,
333 {
334 fn from(value: T) -> Self {
335 Range::new_single_value(value)
336 }
337 }
338
339 impl<T: PartialEq + Display> Display for Range<T> {
340 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
341 match &self.lower {
342 Limit::Inclusive(value) if self.lower == self.upper => value.fmt(f),
343 _ => f.write_fmt(format_args!("range::[{},{}]", self.lower, self.upper)),
344 }
345 }
346 }
347
348 impl<T: WriteAsIon + PartialEq> WriteAsIon for Range<T> {
349 fn write_as_ion<V: ValueWriter>(&self, writer: V) -> IonResult<()> {
350 match &self.lower {
351 Limit::Inclusive(value) if self.lower == self.upper => writer.write(value),
352 _ => writer
353 .with_annotations(["range"])?
354 .write([&self.lower, &self.upper]),
355 }
356 }
357 }
358}
359
360#[cfg(test)]
361mod tests {
362 use crate::isl::ranges::base::Range;
366 use crate::isl::ranges::{I64Range, Limit};
367 use crate::IonSchemaResult;
368 use ion_rs::Element;
369 use rstest::*;
370
371 #[rstest(
372 case::range_with_min_max("range::[min, max]"),
373 case::range_with_max_lower_bound("range::[max, 5]"),
374 case::range_with_min_upper_bound("range::[5, min]"),
375 case::range_with_lower_bound_greater_than_upper_bound("range::[10, 5]"),
376 case::empty_range_with_exclusive_lower_bound("range::[exclusive::5, 5]"),
377 case::empty_range_with_exclusive_upper_bound("range::[5, exclusive::5]"),
378 case::empty_range_with_mutually_excluding_bounds("range::[exclusive::5, exclusive::5]"),
379 case::range_with_unknown_annotation("range::[0, foo::5]"),
380 case::range_without_range_annotation("[5, 10]")
381 )]
382 fn invalid_ranges_from_isl(#[case] range_ion: &str) {
383 let range =
384 I64Range::from_ion_element(&Element::read_one(range_ion).unwrap(), Element::as_i64);
385 assert!(range.is_err());
386 }
387
388 #[rstest(
389 case::range_with_both_limits_unbounded(Range::new(Limit::Min, Limit::Max)),
390 case::range_with_lower_bound_greater_than_upper_bound(Range::new(
391 Limit::Inclusive(3),
392 Limit::Inclusive(1)
393 )),
394 case::range_with_lower_bound_greater_than_upper_bound(Range::new_inclusive(3, 1)),
395 case::empty_range_with_exclusive_lower_bound(Range::new(
396 Limit::Exclusive(3),
397 Limit::Inclusive(3)
398 )),
399 case::empty_range_with_exclusive_upper_bound(Range::new(
400 Limit::Inclusive(3),
401 Limit::Exclusive(3)
402 ))
403 )]
404 fn invalid_ranges_from_constructor(#[case] range_result: IonSchemaResult<Range<i64>>) {
405 assert!(range_result.is_err());
406 }
407
408 #[rstest(
409 case::lower_is_unbounded(
410 Range::new(Limit::Min, Limit::Inclusive(5)),
411 vec![-128, 0, 5],
412 vec![6, 127],
413 ),
414 case::upper_is_unbounded(
415 Range::new(Limit::Inclusive(5), Limit::Max),
416 vec![5, 6, 127],
417 vec![-128, 0, 4],
418 ),
419 case::lower_bound_is_exclusive(
420 Range::new(Limit::Exclusive(5), Limit::Inclusive(10)),
421 vec![6, 9, 10],
422 vec![0, 5, 11],
423 ),
424 case::upper_bound_is_exclusive(
425 Range::new(Limit::Inclusive(5), Limit::Exclusive(10)),
426 vec![5, 6, 9],
427 vec![0, 4, 10],
428 )
429 )]
430 fn range_contains(
431 #[case] range: IonSchemaResult<Range<i64>>,
432 #[case] valid_values: Vec<i64>,
433 #[case] invalid_values: Vec<i64>,
434 ) {
435 let range = range.unwrap();
436 for valid_value in valid_values {
437 let range_contains_result = range.contains(&valid_value);
438 assert!(
439 range_contains_result,
440 "Value {valid_value} was not in {range}"
441 )
442 }
443 for invalid_value in invalid_values {
444 let range_contains_result = range.contains(&invalid_value);
445 assert!(
446 !range_contains_result,
447 "Value {invalid_value} was unexpectedly in {range}"
448 )
449 }
450 }
451
452 #[rstest(
453 case::a_very_simple_case("range::[0,1]", Range::new(Limit::Inclusive(0), Limit::Inclusive(1)).unwrap()),
454 case::lower_is_unbounded("range::[min,1]", Range::new(Limit::Min, Limit::Inclusive(1)).unwrap()),
455 case::upper_is_unbounded("range::[1,max]", Range::new(Limit::Inclusive(1), Limit::Max).unwrap()),
456 case::lower_equals_upper("1", Range::new_single_value(1)),
457 case::lower_is_exclusive("range::[exclusive::0,2]", Range::new(Limit::Exclusive(0), Limit::Inclusive(2)).unwrap()),
458 case::upper_is_exclusive("range::[0,exclusive::2]", Range::new(Limit::Inclusive(0), Limit::Exclusive(2)).unwrap()),
459 case::upper_is_exclusive("1", Range::new(Limit::Inclusive(1), Limit::Inclusive(1)).unwrap()),
461 )]
462 fn range_display(#[case] expected: &str, #[case] range: Range<i64>) {
463 assert_eq!(expected, format!("{range}"));
464 }
465}