datafusion_functions/math/
monotonicity.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18use std::sync::LazyLock;
19
20use datafusion_common::{exec_err, Result, ScalarValue};
21use datafusion_expr::interval_arithmetic::Interval;
22use datafusion_expr::scalar_doc_sections::DOC_SECTION_MATH;
23use datafusion_expr::sort_properties::{ExprProperties, SortProperties};
24use datafusion_expr::Documentation;
25
26/// Non-increasing on the interval \[−1, 1\], undefined otherwise.
27pub fn acos_order(input: &[ExprProperties]) -> Result<SortProperties> {
28    let arg = &input[0];
29    let range = &arg.range;
30
31    let valid_domain =
32        Interval::make_symmetric_unit_interval(&range.lower().data_type())?;
33
34    if valid_domain.contains(range)? == Interval::CERTAINLY_TRUE {
35        Ok(-arg.sort_properties)
36    } else {
37        exec_err!("Input range of ACOS contains out-of-domain values")
38    }
39}
40
41static DOCUMENTATION_ACOS: LazyLock<Documentation> = LazyLock::new(|| {
42    Documentation::builder(
43        DOC_SECTION_MATH,
44        "Returns the arc cosine or inverse cosine of a number.",
45        "acos(numeric_expression)",
46    )
47    .with_standard_argument("numeric_expression", Some("Numeric"))
48    .build()
49});
50
51pub fn get_acos_doc() -> &'static Documentation {
52    &DOCUMENTATION_ACOS
53}
54
55/// Non-decreasing for x ≥ 1, undefined otherwise.
56pub fn acosh_order(input: &[ExprProperties]) -> Result<SortProperties> {
57    let arg = &input[0];
58    let range = &arg.range;
59
60    let valid_domain = Interval::try_new(
61        ScalarValue::new_one(&range.lower().data_type())?,
62        ScalarValue::try_from(&range.upper().data_type())?,
63    )?;
64
65    if valid_domain.contains(range)? == Interval::CERTAINLY_TRUE {
66        Ok(arg.sort_properties)
67    } else {
68        exec_err!("Input range of ACOSH contains out-of-domain values")
69    }
70}
71
72static DOCUMENTATION_ACOSH: LazyLock<Documentation> = LazyLock::new(|| {
73    Documentation::builder(
74        DOC_SECTION_MATH,
75        "Returns the area hyperbolic cosine or inverse hyperbolic cosine of a number.",
76        "acosh(numeric_expression)",
77    )
78    .with_standard_argument("numeric_expression", Some("Numeric"))
79    .build()
80});
81
82pub fn get_acosh_doc() -> &'static Documentation {
83    &DOCUMENTATION_ACOSH
84}
85
86/// Non-decreasing on the interval \[−1, 1\], undefined otherwise.
87pub fn asin_order(input: &[ExprProperties]) -> Result<SortProperties> {
88    let arg = &input[0];
89    let range = &arg.range;
90
91    let valid_domain =
92        Interval::make_symmetric_unit_interval(&range.lower().data_type())?;
93
94    if valid_domain.contains(range)? == Interval::CERTAINLY_TRUE {
95        Ok(arg.sort_properties)
96    } else {
97        exec_err!("Input range of ASIN contains out-of-domain values")
98    }
99}
100
101static DOCUMENTATION_ASIN: LazyLock<Documentation> = LazyLock::new(|| {
102    Documentation::builder(
103        DOC_SECTION_MATH,
104        "Returns the arc sine or inverse sine of a number.",
105        "asin(numeric_expression)",
106    )
107    .with_standard_argument("numeric_expression", Some("Numeric"))
108    .build()
109});
110
111pub fn get_asin_doc() -> &'static Documentation {
112    &DOCUMENTATION_ASIN
113}
114
115/// Non-decreasing for all real numbers.
116pub fn asinh_order(input: &[ExprProperties]) -> Result<SortProperties> {
117    Ok(input[0].sort_properties)
118}
119
120static DOCUMENTATION_ASINH: LazyLock<Documentation> = LazyLock::new(|| {
121    Documentation::builder(
122        DOC_SECTION_MATH,
123        "Returns the area hyperbolic sine or inverse hyperbolic sine of a number.",
124        "asinh(numeric_expression)",
125    )
126    .with_standard_argument("numeric_expression", Some("Numeric"))
127    .build()
128});
129
130pub fn get_asinh_doc() -> &'static Documentation {
131    &DOCUMENTATION_ASINH
132}
133
134/// Non-decreasing for all real numbers.
135pub fn atan_order(input: &[ExprProperties]) -> Result<SortProperties> {
136    Ok(input[0].sort_properties)
137}
138
139static DOCUMENTATION_ATAN: LazyLock<Documentation> = LazyLock::new(|| {
140    Documentation::builder(
141        DOC_SECTION_MATH,
142        "Returns the arc tangent or inverse tangent of a number.",
143        "atan(numeric_expression)",
144    )
145    .with_standard_argument("numeric_expression", Some("Numeric"))
146    .build()
147});
148
149pub fn get_atan_doc() -> &'static Documentation {
150    &DOCUMENTATION_ATAN
151}
152
153/// Non-decreasing on the interval \[−1, 1\], undefined otherwise.
154pub fn atanh_order(input: &[ExprProperties]) -> Result<SortProperties> {
155    let arg = &input[0];
156    let range = &arg.range;
157
158    let valid_domain =
159        Interval::make_symmetric_unit_interval(&range.lower().data_type())?;
160
161    if valid_domain.contains(range)? == Interval::CERTAINLY_TRUE {
162        Ok(arg.sort_properties)
163    } else {
164        exec_err!("Input range of ATANH contains out-of-domain values")
165    }
166}
167
168static DOCUMENTATION_ATANH: LazyLock<Documentation> = LazyLock::new(|| {
169    Documentation::builder(
170        DOC_SECTION_MATH,
171        "Returns the area hyperbolic tangent or inverse hyperbolic tangent of a number.",
172        "atanh(numeric_expression)",
173    )
174    .with_standard_argument("numeric_expression", Some("Numeric"))
175    .build()
176});
177
178pub fn get_atanh_doc() -> &'static Documentation {
179    &DOCUMENTATION_ATANH
180}
181
182/// Order depends on the quadrant.
183// TODO: Implement ordering rule of the ATAN2 function.
184pub fn atan2_order(_input: &[ExprProperties]) -> Result<SortProperties> {
185    Ok(SortProperties::Unordered)
186}
187
188static DOCUMENTATION_ATANH2: LazyLock<Documentation> = LazyLock::new(|| {
189    Documentation::builder(
190        DOC_SECTION_MATH,
191        "Returns the arc tangent or inverse tangent of `expression_y / expression_x`.",
192        "atan2(expression_y, expression_x)",
193    )
194    .with_argument(
195        "expression_y",
196        r#"First numeric expression to operate on.
197Can be a constant, column, or function, and any combination of arithmetic operators."#,
198    )
199    .with_argument(
200        "expression_x",
201        r#"Second numeric expression to operate on.
202Can be a constant, column, or function, and any combination of arithmetic operators."#,
203    )
204    .build()
205});
206
207pub fn get_atan2_doc() -> &'static Documentation {
208    &DOCUMENTATION_ATANH2
209}
210
211/// Non-decreasing for all real numbers.
212pub fn cbrt_order(input: &[ExprProperties]) -> Result<SortProperties> {
213    Ok(input[0].sort_properties)
214}
215
216static DOCUMENTATION_CBRT: LazyLock<Documentation> = LazyLock::new(|| {
217    Documentation::builder(
218        DOC_SECTION_MATH,
219        "Returns the cube root of a number.",
220        "cbrt(numeric_expression)",
221    )
222    .with_standard_argument("numeric_expression", Some("Numeric"))
223    .build()
224});
225
226pub fn get_cbrt_doc() -> &'static Documentation {
227    &DOCUMENTATION_CBRT
228}
229
230/// Non-decreasing for all real numbers.
231pub fn ceil_order(input: &[ExprProperties]) -> Result<SortProperties> {
232    Ok(input[0].sort_properties)
233}
234
235static DOCUMENTATION_CEIL: LazyLock<Documentation> = LazyLock::new(|| {
236    Documentation::builder(
237        DOC_SECTION_MATH,
238        "Returns the nearest integer greater than or equal to a number.",
239        "ceil(numeric_expression)",
240    )
241    .with_standard_argument("numeric_expression", Some("Numeric"))
242    .build()
243});
244
245pub fn get_ceil_doc() -> &'static Documentation {
246    &DOCUMENTATION_CEIL
247}
248
249/// Non-increasing on \[0, π\] and then non-decreasing on \[π, 2π\].
250/// This pattern repeats periodically with a period of 2π.
251// TODO: Implement ordering rule of the ATAN2 function.
252pub fn cos_order(_input: &[ExprProperties]) -> Result<SortProperties> {
253    Ok(SortProperties::Unordered)
254}
255
256static DOCUMENTATION_COS: LazyLock<Documentation> = LazyLock::new(|| {
257    Documentation::builder(
258        DOC_SECTION_MATH,
259        "Returns the cosine of a number.",
260        "cos(numeric_expression)",
261    )
262    .with_standard_argument("numeric_expression", Some("Numeric"))
263    .build()
264});
265
266pub fn get_cos_doc() -> &'static Documentation {
267    &DOCUMENTATION_COS
268}
269
270/// Non-decreasing for x ≥ 0 and symmetrically non-increasing for x ≤ 0.
271pub fn cosh_order(input: &[ExprProperties]) -> Result<SortProperties> {
272    let arg = &input[0];
273    let range = &arg.range;
274
275    let zero_point = Interval::make_zero(&range.lower().data_type())?;
276
277    if range.gt_eq(&zero_point)? == Interval::CERTAINLY_TRUE {
278        Ok(arg.sort_properties)
279    } else if range.lt_eq(&zero_point)? == Interval::CERTAINLY_TRUE {
280        Ok(-arg.sort_properties)
281    } else {
282        Ok(SortProperties::Unordered)
283    }
284}
285
286static DOCUMENTATION_COSH: LazyLock<Documentation> = LazyLock::new(|| {
287    Documentation::builder(
288        DOC_SECTION_MATH,
289        "Returns the hyperbolic cosine of a number.",
290        "cosh(numeric_expression)",
291    )
292    .with_standard_argument("numeric_expression", Some("Numeric"))
293    .build()
294});
295
296pub fn get_cosh_doc() -> &'static Documentation {
297    &DOCUMENTATION_COSH
298}
299
300/// Non-decreasing function that converts radians to degrees.
301pub fn degrees_order(input: &[ExprProperties]) -> Result<SortProperties> {
302    Ok(input[0].sort_properties)
303}
304
305static DOCUMENTATION_DEGREES: LazyLock<Documentation> = LazyLock::new(|| {
306    Documentation::builder(
307        DOC_SECTION_MATH,
308        "Converts radians to degrees.",
309        "degrees(numeric_expression)",
310    )
311    .with_standard_argument("numeric_expression", Some("Numeric"))
312    .build()
313});
314
315pub fn get_degrees_doc() -> &'static Documentation {
316    &DOCUMENTATION_DEGREES
317}
318
319/// Non-decreasing for all real numbers.
320pub fn exp_order(input: &[ExprProperties]) -> Result<SortProperties> {
321    Ok(input[0].sort_properties)
322}
323
324static DOCUMENTATION_EXP: LazyLock<Documentation> = LazyLock::new(|| {
325    Documentation::builder(
326        DOC_SECTION_MATH,
327        "Returns the base-e exponential of a number.",
328        "exp(numeric_expression)",
329    )
330    .with_standard_argument("numeric_expression", Some("Numeric"))
331    .build()
332});
333
334pub fn get_exp_doc() -> &'static Documentation {
335    &DOCUMENTATION_EXP
336}
337
338/// Non-decreasing for all real numbers.
339pub fn floor_order(input: &[ExprProperties]) -> Result<SortProperties> {
340    Ok(input[0].sort_properties)
341}
342
343static DOCUMENTATION_FLOOR: LazyLock<Documentation> = LazyLock::new(|| {
344    Documentation::builder(
345        DOC_SECTION_MATH,
346        "Returns the nearest integer less than or equal to a number.",
347        "floor(numeric_expression)",
348    )
349    .with_standard_argument("numeric_expression", Some("Numeric"))
350    .build()
351});
352
353pub fn get_floor_doc() -> &'static Documentation {
354    &DOCUMENTATION_FLOOR
355}
356
357/// Non-decreasing for x ≥ 0, undefined otherwise.
358pub fn ln_order(input: &[ExprProperties]) -> Result<SortProperties> {
359    let arg = &input[0];
360    let range = &arg.range;
361
362    let zero_point = Interval::make_zero(&range.lower().data_type())?;
363
364    if range.gt_eq(&zero_point)? == Interval::CERTAINLY_TRUE {
365        Ok(arg.sort_properties)
366    } else {
367        exec_err!("Input range of LN contains out-of-domain values")
368    }
369}
370
371static DOCUMENTATION_LN: LazyLock<Documentation> = LazyLock::new(|| {
372    Documentation::builder(
373        DOC_SECTION_MATH,
374        "Returns the natural logarithm of a number.",
375        "ln(numeric_expression)",
376    )
377    .with_standard_argument("numeric_expression", Some("Numeric"))
378    .build()
379});
380
381pub fn get_ln_doc() -> &'static Documentation {
382    &DOCUMENTATION_LN
383}
384
385/// Non-decreasing for x ≥ 0, undefined otherwise.
386pub fn log2_order(input: &[ExprProperties]) -> Result<SortProperties> {
387    let arg = &input[0];
388    let range = &arg.range;
389
390    let zero_point = Interval::make_zero(&range.lower().data_type())?;
391
392    if range.gt_eq(&zero_point)? == Interval::CERTAINLY_TRUE {
393        Ok(arg.sort_properties)
394    } else {
395        exec_err!("Input range of LOG2 contains out-of-domain values")
396    }
397}
398
399static DOCUMENTATION_LOG2: LazyLock<Documentation> = LazyLock::new(|| {
400    Documentation::builder(
401        DOC_SECTION_MATH,
402        "Returns the base-2 logarithm of a number.",
403        "log2(numeric_expression)",
404    )
405    .with_standard_argument("numeric_expression", Some("Numeric"))
406    .build()
407});
408
409pub fn get_log2_doc() -> &'static Documentation {
410    &DOCUMENTATION_LOG2
411}
412
413/// Non-decreasing for x ≥ 0, undefined otherwise.
414pub fn log10_order(input: &[ExprProperties]) -> Result<SortProperties> {
415    let arg = &input[0];
416    let range = &arg.range;
417
418    let zero_point = Interval::make_zero(&range.lower().data_type())?;
419
420    if range.gt_eq(&zero_point)? == Interval::CERTAINLY_TRUE {
421        Ok(arg.sort_properties)
422    } else {
423        exec_err!("Input range of LOG10 contains out-of-domain values")
424    }
425}
426
427static DOCUMENTATION_LOG10: LazyLock<Documentation> = LazyLock::new(|| {
428    Documentation::builder(
429        DOC_SECTION_MATH,
430        "Returns the base-10 logarithm of a number.",
431        "log10(numeric_expression)",
432    )
433    .with_standard_argument("numeric_expression", Some("Numeric"))
434    .build()
435});
436
437pub fn get_log10_doc() -> &'static Documentation {
438    &DOCUMENTATION_LOG10
439}
440
441/// Non-decreasing for all real numbers x.
442pub fn radians_order(input: &[ExprProperties]) -> Result<SortProperties> {
443    Ok(input[0].sort_properties)
444}
445
446static DOCUMENTATION_RADIONS: LazyLock<Documentation> = LazyLock::new(|| {
447    Documentation::builder(
448        DOC_SECTION_MATH,
449        "Converts degrees to radians.",
450        "radians(numeric_expression)",
451    )
452    .with_standard_argument("numeric_expression", Some("Numeric"))
453    .build()
454});
455
456pub fn get_radians_doc() -> &'static Documentation {
457    &DOCUMENTATION_RADIONS
458}
459
460/// Non-decreasing on \[0, π\] and then non-increasing on \[π, 2π\].
461/// This pattern repeats periodically with a period of 2π.
462// TODO: Implement ordering rule of the SIN function.
463pub fn sin_order(_input: &[ExprProperties]) -> Result<SortProperties> {
464    Ok(SortProperties::Unordered)
465}
466
467static DOCUMENTATION_SIN: LazyLock<Documentation> = LazyLock::new(|| {
468    Documentation::builder(
469        DOC_SECTION_MATH,
470        "Returns the sine of a number.",
471        "sin(numeric_expression)",
472    )
473    .with_standard_argument("numeric_expression", Some("Numeric"))
474    .build()
475});
476
477pub fn get_sin_doc() -> &'static Documentation {
478    &DOCUMENTATION_SIN
479}
480
481/// Non-decreasing for all real numbers.
482pub fn sinh_order(input: &[ExprProperties]) -> Result<SortProperties> {
483    Ok(input[0].sort_properties)
484}
485
486static DOCUMENTATION_SINH: LazyLock<Documentation> = LazyLock::new(|| {
487    Documentation::builder(
488        DOC_SECTION_MATH,
489        "Returns the hyperbolic sine of a number.",
490        "sinh(numeric_expression)",
491    )
492    .with_standard_argument("numeric_expression", Some("Numeric"))
493    .build()
494});
495
496pub fn get_sinh_doc() -> &'static Documentation {
497    &DOCUMENTATION_SINH
498}
499
500/// Non-decreasing for x ≥ 0, undefined otherwise.
501pub fn sqrt_order(input: &[ExprProperties]) -> Result<SortProperties> {
502    let arg = &input[0];
503    let range = &arg.range;
504
505    let zero_point = Interval::make_zero(&range.lower().data_type())?;
506
507    if range.gt_eq(&zero_point)? == Interval::CERTAINLY_TRUE {
508        Ok(arg.sort_properties)
509    } else {
510        exec_err!("Input range of SQRT contains out-of-domain values")
511    }
512}
513
514static DOCUMENTATION_SQRT: LazyLock<Documentation> = LazyLock::new(|| {
515    Documentation::builder(
516        DOC_SECTION_MATH,
517        "Returns the square root of a number.",
518        "sqrt(numeric_expression)",
519    )
520    .with_standard_argument("numeric_expression", Some("Numeric"))
521    .build()
522});
523
524pub fn get_sqrt_doc() -> &'static Documentation {
525    &DOCUMENTATION_SQRT
526}
527
528/// Non-decreasing between vertical asymptotes at x = k * π ± π / 2 for any
529/// integer k.
530// TODO: Implement ordering rule of the TAN function.
531pub fn tan_order(_input: &[ExprProperties]) -> Result<SortProperties> {
532    Ok(SortProperties::Unordered)
533}
534
535static DOCUMENTATION_TAN: LazyLock<Documentation> = LazyLock::new(|| {
536    Documentation::builder(
537        DOC_SECTION_MATH,
538        "Returns the tangent of a number.",
539        "tan(numeric_expression)",
540    )
541    .with_standard_argument("numeric_expression", Some("Numeric"))
542    .build()
543});
544
545pub fn get_tan_doc() -> &'static Documentation {
546    &DOCUMENTATION_TAN
547}
548
549/// Non-decreasing for all real numbers.
550pub fn tanh_order(input: &[ExprProperties]) -> Result<SortProperties> {
551    Ok(input[0].sort_properties)
552}
553
554static DOCUMENTATION_TANH: LazyLock<Documentation> = LazyLock::new(|| {
555    Documentation::builder(
556        DOC_SECTION_MATH,
557        "Returns the hyperbolic tangent of a number.",
558        "tanh(numeric_expression)",
559    )
560    .with_standard_argument("numeric_expression", Some("Numeric"))
561    .build()
562});
563
564pub fn get_tanh_doc() -> &'static Documentation {
565    &DOCUMENTATION_TANH
566}
567
568#[cfg(test)]
569mod tests {
570    use arrow::compute::SortOptions;
571    use datafusion_common::Result;
572
573    use super::*;
574
575    #[derive(Debug)]
576    struct MonotonicityTestCase {
577        name: &'static str,
578        func: fn(&[ExprProperties]) -> Result<SortProperties>,
579        lower: f64,
580        upper: f64,
581        input_sort: SortProperties,
582        expected: Result<SortProperties>,
583    }
584
585    #[test]
586    fn test_monotonicity_table() {
587        fn create_ep(lower: f64, upper: f64, sp: SortProperties) -> ExprProperties {
588            ExprProperties {
589                range: Interval::try_new(
590                    ScalarValue::from(lower),
591                    ScalarValue::from(upper),
592                )
593                .unwrap(),
594                sort_properties: sp,
595                preserves_lex_ordering: false,
596            }
597        }
598
599        let test_cases = vec![
600            MonotonicityTestCase {
601                name: "acos_order within domain",
602                func: acos_order,
603                lower: -0.5,
604                upper: 0.5,
605                input_sort: SortProperties::Ordered(SortOptions {
606                    descending: false,
607                    nulls_first: false,
608                }),
609                expected: Ok(SortProperties::Ordered(SortOptions {
610                    descending: true,
611                    nulls_first: false,
612                })),
613            },
614            MonotonicityTestCase {
615                name: "acos_order out of domain",
616                func: acos_order,
617                lower: -2.0,
618                upper: 1.0,
619                input_sort: SortProperties::Ordered(SortOptions {
620                    descending: false,
621                    nulls_first: false,
622                }),
623                expected: exec_err!("Input range of ACOS contains out-of-domain values"),
624            },
625            MonotonicityTestCase {
626                name: "acosh_order within domain",
627                func: acosh_order,
628                lower: 2.0,
629                upper: 100.0,
630                input_sort: SortProperties::Ordered(SortOptions {
631                    descending: false,
632                    nulls_first: true,
633                }),
634                expected: Ok(SortProperties::Ordered(SortOptions {
635                    descending: false,
636                    nulls_first: true,
637                })),
638            },
639            MonotonicityTestCase {
640                name: "acosh_order out of domain",
641                func: acosh_order,
642                lower: 0.5,
643                upper: 1.0,
644                input_sort: SortProperties::Ordered(SortOptions {
645                    descending: true,
646                    nulls_first: false,
647                }),
648                expected: exec_err!("Input range of ACOSH contains out-of-domain values"),
649            },
650            MonotonicityTestCase {
651                name: "asin_order within domain",
652                func: asin_order,
653                lower: -0.5,
654                upper: 0.5,
655                input_sort: SortProperties::Ordered(SortOptions {
656                    descending: false,
657                    nulls_first: false,
658                }),
659                expected: Ok(SortProperties::Ordered(SortOptions {
660                    descending: false,
661                    nulls_first: false,
662                })),
663            },
664            MonotonicityTestCase {
665                name: "asin_order out of domain",
666                func: asin_order,
667                lower: -2.0,
668                upper: 1.0,
669                input_sort: SortProperties::Ordered(SortOptions {
670                    descending: false,
671                    nulls_first: false,
672                }),
673                expected: exec_err!("Input range of ASIN contains out-of-domain values"),
674            },
675            MonotonicityTestCase {
676                name: "asinh_order within domain",
677                func: asinh_order,
678                lower: -1.0,
679                upper: 1.0,
680                input_sort: SortProperties::Ordered(SortOptions {
681                    descending: false,
682                    nulls_first: false,
683                }),
684                expected: Ok(SortProperties::Ordered(SortOptions {
685                    descending: false,
686                    nulls_first: false,
687                })),
688            },
689            MonotonicityTestCase {
690                name: "asinh_order out of domain",
691                func: asinh_order,
692                lower: -2.0,
693                upper: 1.0,
694                input_sort: SortProperties::Ordered(SortOptions {
695                    descending: false,
696                    nulls_first: false,
697                }),
698                expected: Ok(SortProperties::Ordered(SortOptions {
699                    descending: false,
700                    nulls_first: false,
701                })),
702            },
703            MonotonicityTestCase {
704                name: "atan_order within domain",
705                func: atan_order,
706                lower: -1.0,
707                upper: 1.0,
708                input_sort: SortProperties::Ordered(SortOptions {
709                    descending: false,
710                    nulls_first: false,
711                }),
712                expected: Ok(SortProperties::Ordered(SortOptions {
713                    descending: false,
714                    nulls_first: false,
715                })),
716            },
717            MonotonicityTestCase {
718                name: "atan_order out of domain",
719                func: atan_order,
720                lower: -2.0,
721                upper: 1.0,
722                input_sort: SortProperties::Ordered(SortOptions {
723                    descending: false,
724                    nulls_first: false,
725                }),
726                expected: Ok(SortProperties::Ordered(SortOptions {
727                    descending: false,
728                    nulls_first: false,
729                })),
730            },
731            MonotonicityTestCase {
732                name: "atanh_order within domain",
733                func: atanh_order,
734                lower: -0.6,
735                upper: 0.6,
736                input_sort: SortProperties::Ordered(SortOptions {
737                    descending: false,
738                    nulls_first: false,
739                }),
740                expected: Ok(SortProperties::Ordered(SortOptions {
741                    descending: false,
742                    nulls_first: false,
743                })),
744            },
745            MonotonicityTestCase {
746                name: "atanh_order out of domain",
747                func: atanh_order,
748                lower: -2.0,
749                upper: 1.0,
750                input_sort: SortProperties::Ordered(SortOptions {
751                    descending: false,
752                    nulls_first: false,
753                }),
754                expected: exec_err!("Input range of ATANH contains out-of-domain values"),
755            },
756            MonotonicityTestCase {
757                name: "cbrt_order within domain",
758                func: cbrt_order,
759                lower: -1.0,
760                upper: 1.0,
761                input_sort: SortProperties::Ordered(SortOptions {
762                    descending: false,
763                    nulls_first: false,
764                }),
765                expected: Ok(SortProperties::Ordered(SortOptions {
766                    descending: false,
767                    nulls_first: false,
768                })),
769            },
770            MonotonicityTestCase {
771                name: "cbrt_order out of domain",
772                func: cbrt_order,
773                lower: -2.0,
774                upper: 1.0,
775                input_sort: SortProperties::Ordered(SortOptions {
776                    descending: false,
777                    nulls_first: false,
778                }),
779                expected: Ok(SortProperties::Ordered(SortOptions {
780                    descending: false,
781                    nulls_first: false,
782                })),
783            },
784            MonotonicityTestCase {
785                name: "ceil_order within domain",
786                func: ceil_order,
787                lower: -1.0,
788                upper: 1.0,
789                input_sort: SortProperties::Ordered(SortOptions {
790                    descending: false,
791                    nulls_first: false,
792                }),
793                expected: Ok(SortProperties::Ordered(SortOptions {
794                    descending: false,
795                    nulls_first: false,
796                })),
797            },
798            MonotonicityTestCase {
799                name: "ceil_order out of domain",
800                func: ceil_order,
801                lower: -2.0,
802                upper: 1.0,
803                input_sort: SortProperties::Ordered(SortOptions {
804                    descending: false,
805                    nulls_first: false,
806                }),
807                expected: Ok(SortProperties::Ordered(SortOptions {
808                    descending: false,
809                    nulls_first: false,
810                })),
811            },
812            MonotonicityTestCase {
813                name: "cos_order within domain",
814                func: cos_order,
815                lower: 0.0,
816                upper: 2.0 * std::f64::consts::PI,
817                input_sort: SortProperties::Ordered(SortOptions {
818                    descending: false,
819                    nulls_first: false,
820                }),
821                expected: Ok(SortProperties::Unordered),
822            },
823            MonotonicityTestCase {
824                name: "cos_order out of domain",
825                func: cos_order,
826                lower: -2.0,
827                upper: 1.0,
828                input_sort: SortProperties::Ordered(SortOptions {
829                    descending: false,
830                    nulls_first: false,
831                }),
832                expected: Ok(SortProperties::Unordered),
833            },
834            MonotonicityTestCase {
835                name: "cosh_order within domain positive",
836                func: cosh_order,
837                lower: 5.0,
838                upper: 100.0,
839                input_sort: SortProperties::Ordered(SortOptions {
840                    descending: false,
841                    nulls_first: false,
842                }),
843                expected: Ok(SortProperties::Ordered(SortOptions {
844                    descending: false,
845                    nulls_first: false,
846                })),
847            },
848            MonotonicityTestCase {
849                name: "cosh_order within domain negative",
850                func: cosh_order,
851                lower: -100.0,
852                upper: -5.0,
853                input_sort: SortProperties::Ordered(SortOptions {
854                    descending: false,
855                    nulls_first: false,
856                }),
857                expected: Ok(SortProperties::Ordered(SortOptions {
858                    descending: true,
859                    nulls_first: false,
860                })),
861            },
862            MonotonicityTestCase {
863                name: "cosh_order out of domain so unordered",
864                func: cosh_order,
865                lower: -1.0,
866                upper: 1.0,
867                input_sort: SortProperties::Ordered(SortOptions {
868                    descending: false,
869                    nulls_first: false,
870                }),
871                expected: Ok(SortProperties::Unordered),
872            },
873            MonotonicityTestCase {
874                name: "degrees_order",
875                func: degrees_order,
876                lower: -1.0,
877                upper: 1.0,
878                input_sort: SortProperties::Ordered(SortOptions {
879                    descending: true,
880                    nulls_first: true,
881                }),
882                expected: Ok(SortProperties::Ordered(SortOptions {
883                    descending: true,
884                    nulls_first: true,
885                })),
886            },
887            MonotonicityTestCase {
888                name: "exp_order",
889                func: exp_order,
890                lower: -1000.0,
891                upper: 1000.0,
892                input_sort: SortProperties::Ordered(SortOptions {
893                    descending: false,
894                    nulls_first: false,
895                }),
896                expected: Ok(SortProperties::Ordered(SortOptions {
897                    descending: false,
898                    nulls_first: false,
899                })),
900            },
901            MonotonicityTestCase {
902                name: "floor_order",
903                func: floor_order,
904                lower: -1.0,
905                upper: 1.0,
906                input_sort: SortProperties::Ordered(SortOptions {
907                    descending: true,
908                    nulls_first: true,
909                }),
910                expected: Ok(SortProperties::Ordered(SortOptions {
911                    descending: true,
912                    nulls_first: true,
913                })),
914            },
915            MonotonicityTestCase {
916                name: "ln_order within domain",
917                func: ln_order,
918                lower: 1.0,
919                upper: 2.0,
920                input_sort: SortProperties::Ordered(SortOptions {
921                    descending: false,
922                    nulls_first: false,
923                }),
924                expected: Ok(SortProperties::Ordered(SortOptions {
925                    descending: false,
926                    nulls_first: false,
927                })),
928            },
929            MonotonicityTestCase {
930                name: "ln_order out of domain",
931                func: ln_order,
932                lower: -5.0,
933                upper: -4.0,
934                input_sort: SortProperties::Ordered(SortOptions {
935                    descending: false,
936                    nulls_first: false,
937                }),
938                expected: exec_err!("Input range of LN contains out-of-domain values"),
939            },
940        ];
941
942        for tcase in test_cases {
943            let input = vec![create_ep(tcase.lower, tcase.upper, tcase.input_sort)];
944            let actual = (tcase.func)(&input);
945            match (&actual, &tcase.expected) {
946                (Ok(a), Ok(e)) => assert_eq!(
947                    a, e,
948                    "Test '{}' failed: got {:?}, expected {:?}",
949                    tcase.name, a, e
950                ),
951                (Err(e1), Err(e2)) => {
952                    assert_eq!(
953                        e1.strip_backtrace().to_string(),
954                        e2.strip_backtrace().to_string(),
955                        "Test '{}' failed: got {:?}, expected {:?}",
956                        tcase.name,
957                        e1,
958                        e2
959                    )
960                } // Both are errors, so it's fine
961                _ => panic!(
962                    "Test '{}' failed: got {:?}, expected {:?}",
963                    tcase.name, actual, tcase.expected
964                ),
965            }
966        }
967    }
968}