Skip to main content

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