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;
169 adjusted_lower >= end_value
170 }
171 _ => false,
172 }
173 }
174}
175
176mod base {
178 use super::*;
179
180 pub trait RangeValidation<T: PartialOrd> {
183 fn is_empty(start: &Limit<T>, end: &Limit<T>) -> bool {
185 match (start, end) {
186 (Limit::Inclusive(lower), Limit::Inclusive(upper)) => lower > upper,
187 (Limit::Exclusive(lower), Limit::Exclusive(upper))
188 | (Limit::Exclusive(lower), Limit::Inclusive(upper))
189 | (Limit::Inclusive(lower), Limit::Exclusive(upper)) => lower >= upper,
190 _ => false,
191 }
192 }
193 }
194
195 pub fn is_int_range_empty<T: CheckedAdd + One + PartialOrd>(
199 start: &Limit<T>,
200 end: &Limit<T>,
201 ) -> bool {
202 match (start, end) {
203 (Limit::Inclusive(lower), Limit::Inclusive(upper)) => lower > upper,
204 (Limit::Exclusive(lower), Limit::Inclusive(upper))
205 | (Limit::Inclusive(lower), Limit::Exclusive(upper)) => lower >= upper,
206 (Limit::Exclusive(lower), Limit::Exclusive(upper)) => {
207 let adjusted_lower = lower.checked_add(&T::one());
209 if adjusted_lower.is_none() {
211 return true;
212 }
213 adjusted_lower.unwrap() >= *upper
214 }
215 _ => false,
216 }
217 }
218
219 #[derive(Debug, Clone, PartialEq)]
224 pub struct Range<T> {
225 lower: Limit<T>,
226 upper: Limit<T>,
227 }
228
229 impl<T: PartialOrd + Clone> Range<T>
232 where
233 Self: RangeValidation<T>,
234 {
235 pub fn new(start: Limit<T>, end: Limit<T>) -> IonSchemaResult<Self> {
238 isl_require!(start != Limit::Min || end != Limit::Max => "range may not contain both 'min' and 'max'")?;
239 isl_require!(start != Limit::Max => "range may not be empty (start of range may not be 'max')")?;
240 isl_require!(end != Limit::Min => "range may not be empty (end of range cannot be 'min')")?;
241 isl_require!(!Self::is_empty(&start, &end) => "range may not be empty")?;
242 Ok(Self {
243 lower: start,
244 upper: end,
245 })
246 }
247
248 pub fn new_inclusive(start: T, end: T) -> IonSchemaResult<Self> {
251 Self::new(Limit::Inclusive(start), Limit::Inclusive(end))
252 }
253
254 pub fn new_single_value(value: T) -> Self {
256 Self::new_inclusive(value.clone(), value).unwrap()
258 }
259
260 pub fn lower(&self) -> &Limit<T> {
261 &self.lower
262 }
263
264 pub fn upper(&self) -> &Limit<T> {
265 &self.upper
266 }
267
268 pub fn contains<V: Into<T> + Clone>(&self, value: &V) -> bool {
270 let other = value.clone().into();
271 self.lower <= other && self.upper >= other
272 }
273
274 pub fn from_ion_element<F: Fn(&Element) -> Option<T>>(
276 element: &Element,
277 value_fn: F,
278 ) -> IonSchemaResult<Range<T>> {
279 if element.annotations().contains("range") {
280 isl_require!(element.ion_type() == IonType::List => "range must be a non-null list; found: {element}")?;
281 isl_require!(!element.is_null() => "range must be a non-null list; found: {element}")?;
282 let seq = element.as_sequence().unwrap();
283 isl_require!(seq.len() == 2 => "range must have a lower and upper bound; found: {element}")?;
284
285 let lower_limit = Self::read_range_bound(element, seq.get(0).unwrap(), &value_fn)?;
286 let upper_limit = Self::read_range_bound(element, seq.get(1).unwrap(), &value_fn)?;
287
288 Self::new(lower_limit, upper_limit)
289 } else {
290 let value = value_fn(element);
291 if let Some(value) = value {
292 Ok(Self::new_single_value(value))
293 } else {
294 invalid_schema_error(format!("invalid value for range: {element}"))
295 }
296 }
297 }
298
299 fn read_range_bound<F: Fn(&Element) -> Option<T>>(
300 element: &Element,
301 boundary_element: &Element,
302 value_fn: F,
303 ) -> IonSchemaResult<Limit<T>> {
304 let is_exclusive = boundary_element.annotations().contains("exclusive");
305 isl_require!(boundary_element.annotations().len() == if is_exclusive {1} else {0} => "invalid annotation(s) on range boundary {element}")?;
306
307 match boundary_element.as_symbol().map(|s| s.text()) {
308 Some(Some("min")) => {
309 isl_require!(!is_exclusive => "'min' may not be exclusive")?;
310 Ok(Limit::Min)
311 }
312 Some(Some("max")) => {
313 isl_require!(!is_exclusive => "'max' may not be exclusive")?;
314 Ok(Limit::Max)
315 }
316 _ => {
317 let limit_value: T = value_fn(boundary_element).ok_or_else(|| {
318 invalid_schema_error_raw(format!(
319 "invalid value for range boundary: {element}"
320 ))
321 })?;
322 if is_exclusive {
323 Ok(Limit::Exclusive(limit_value))
324 } else {
325 Ok(Limit::Inclusive(limit_value))
326 }
327 }
328 }
329 }
330 }
331
332 impl<T: Clone + PartialEq + PartialOrd> From<T> for Range<T>
333 where
334 Self: RangeValidation<T>,
335 {
336 fn from(value: T) -> Self {
337 Range::new_single_value(value)
338 }
339 }
340
341 impl<T: PartialEq + Display> Display for Range<T> {
342 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
343 match &self.lower {
344 Limit::Inclusive(value) if self.lower == self.upper => value.fmt(f),
345 _ => f.write_fmt(format_args!("range::[{},{}]", self.lower, self.upper)),
346 }
347 }
348 }
349
350 impl<T: WriteAsIon + PartialEq> WriteAsIon for Range<T> {
351 fn write_as_ion<V: ValueWriter>(&self, writer: V) -> IonResult<()> {
352 match &self.lower {
353 Limit::Inclusive(value) if self.lower == self.upper => writer.write(value),
354 _ => writer
355 .with_annotations(["range"])?
356 .write([&self.lower, &self.upper]),
357 }
358 }
359 }
360}
361
362#[cfg(test)]
363mod tests {
364 use crate::isl::ranges::base::Range;
368 use crate::isl::ranges::{I64Range, Limit};
369 use crate::IonSchemaResult;
370 use ion_rs::Element;
371 use rstest::*;
372
373 #[rstest(
374 case::range_with_min_max("range::[min, max]"),
375 case::range_with_max_lower_bound("range::[max, 5]"),
376 case::range_with_min_upper_bound("range::[5, min]"),
377 case::range_with_lower_bound_greater_than_upper_bound("range::[10, 5]"),
378 case::empty_range_with_exclusive_lower_bound("range::[exclusive::5, 5]"),
379 case::empty_range_with_exclusive_upper_bound("range::[5, exclusive::5]"),
380 case::empty_range_with_mutually_excluding_bounds("range::[exclusive::5, exclusive::5]"),
381 case::range_with_unknown_annotation("range::[0, foo::5]"),
382 case::range_without_range_annotation("[5, 10]")
383 )]
384 fn invalid_ranges_from_isl(#[case] range_ion: &str) {
385 let range =
386 I64Range::from_ion_element(&Element::read_one(range_ion).unwrap(), Element::as_i64);
387 assert!(range.is_err());
388 }
389
390 #[rstest(
391 case::range_with_both_limits_unbounded(Range::new(Limit::Min, Limit::Max)),
392 case::range_with_lower_bound_greater_than_upper_bound(Range::new(
393 Limit::Inclusive(3),
394 Limit::Inclusive(1)
395 )),
396 case::range_with_lower_bound_greater_than_upper_bound(Range::new_inclusive(3, 1)),
397 case::empty_range_with_exclusive_lower_bound(Range::new(
398 Limit::Exclusive(3),
399 Limit::Inclusive(3)
400 )),
401 case::empty_range_with_exclusive_upper_bound(Range::new(
402 Limit::Inclusive(3),
403 Limit::Exclusive(3)
404 ))
405 )]
406 fn invalid_ranges_from_constructor(#[case] range_result: IonSchemaResult<Range<i64>>) {
407 assert!(range_result.is_err());
408 }
409
410 #[rstest(
411 case::lower_is_unbounded(
412 Range::new(Limit::Min, Limit::Inclusive(5)),
413 vec![-128, 0, 5],
414 vec![6, 127],
415 ),
416 case::upper_is_unbounded(
417 Range::new(Limit::Inclusive(5), Limit::Max),
418 vec![5, 6, 127],
419 vec![-128, 0, 4],
420 ),
421 case::lower_bound_is_exclusive(
422 Range::new(Limit::Exclusive(5), Limit::Inclusive(10)),
423 vec![6, 9, 10],
424 vec![0, 5, 11],
425 ),
426 case::upper_bound_is_exclusive(
427 Range::new(Limit::Inclusive(5), Limit::Exclusive(10)),
428 vec![5, 6, 9],
429 vec![0, 4, 10],
430 )
431 )]
432 fn range_contains(
433 #[case] range: IonSchemaResult<Range<i64>>,
434 #[case] valid_values: Vec<i64>,
435 #[case] invalid_values: Vec<i64>,
436 ) {
437 let range = range.unwrap();
438 for valid_value in valid_values {
439 let range_contains_result = range.contains(&valid_value);
440 assert!(
441 range_contains_result,
442 "Value {valid_value} was not in {range}"
443 )
444 }
445 for invalid_value in invalid_values {
446 let range_contains_result = range.contains(&invalid_value);
447 assert!(
448 !range_contains_result,
449 "Value {invalid_value} was unexpectedly in {range}"
450 )
451 }
452 }
453
454 #[rstest(
455 case::a_very_simple_case("range::[0,1]", Range::new(Limit::Inclusive(0), Limit::Inclusive(1)).unwrap()),
456 case::lower_is_unbounded("range::[min,1]", Range::new(Limit::Min, Limit::Inclusive(1)).unwrap()),
457 case::upper_is_unbounded("range::[1,max]", Range::new(Limit::Inclusive(1), Limit::Max).unwrap()),
458 case::lower_equals_upper("1", Range::new_single_value(1)),
459 case::lower_is_exclusive("range::[exclusive::0,2]", Range::new(Limit::Exclusive(0), Limit::Inclusive(2)).unwrap()),
460 case::upper_is_exclusive("range::[0,exclusive::2]", Range::new(Limit::Inclusive(0), Limit::Exclusive(2)).unwrap()),
461 case::upper_is_exclusive("1", Range::new(Limit::Inclusive(1), Limit::Inclusive(1)).unwrap()),
463 )]
464 fn range_display(#[case] expected: &str, #[case] range: Range<i64>) {
465 assert_eq!(expected, format!("{range}"));
466 }
467}