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    use datafusion_common::Result;
741
742    use super::*;
743
744    #[derive(Debug)]
745    struct MonotonicityTestCase {
746        name: &'static str,
747        func: fn(&[ExprProperties]) -> Result<SortProperties>,
748        lower: f64,
749        upper: f64,
750        input_sort: SortProperties,
751        expected: Result<SortProperties>,
752    }
753
754    #[test]
755    fn test_monotonicity_table() {
756        fn create_ep(lower: f64, upper: f64, sp: SortProperties) -> ExprProperties {
757            ExprProperties {
758                range: Interval::try_new(
759                    ScalarValue::from(lower),
760                    ScalarValue::from(upper),
761                )
762                .unwrap(),
763                sort_properties: sp,
764                preserves_lex_ordering: false,
765            }
766        }
767
768        let test_cases = vec![
769            MonotonicityTestCase {
770                name: "acos_order within domain",
771                func: acos_order,
772                lower: -0.5,
773                upper: 0.5,
774                input_sort: SortProperties::Ordered(SortOptions {
775                    descending: false,
776                    nulls_first: false,
777                }),
778                expected: Ok(SortProperties::Ordered(SortOptions {
779                    descending: true,
780                    nulls_first: false,
781                })),
782            },
783            MonotonicityTestCase {
784                name: "acos_order out of domain",
785                func: acos_order,
786                lower: -2.0,
787                upper: 1.0,
788                input_sort: SortProperties::Ordered(SortOptions {
789                    descending: false,
790                    nulls_first: false,
791                }),
792                expected: exec_err!("Input range of ACOS contains out-of-domain values"),
793            },
794            MonotonicityTestCase {
795                name: "acosh_order within domain",
796                func: acosh_order,
797                lower: 2.0,
798                upper: 100.0,
799                input_sort: SortProperties::Ordered(SortOptions {
800                    descending: false,
801                    nulls_first: true,
802                }),
803                expected: Ok(SortProperties::Ordered(SortOptions {
804                    descending: false,
805                    nulls_first: true,
806                })),
807            },
808            MonotonicityTestCase {
809                name: "acosh_order out of domain",
810                func: acosh_order,
811                lower: 0.5,
812                upper: 1.0,
813                input_sort: SortProperties::Ordered(SortOptions {
814                    descending: true,
815                    nulls_first: false,
816                }),
817                expected: exec_err!("Input range of ACOSH contains out-of-domain values"),
818            },
819            MonotonicityTestCase {
820                name: "asin_order within domain",
821                func: asin_order,
822                lower: -0.5,
823                upper: 0.5,
824                input_sort: SortProperties::Ordered(SortOptions {
825                    descending: false,
826                    nulls_first: false,
827                }),
828                expected: Ok(SortProperties::Ordered(SortOptions {
829                    descending: false,
830                    nulls_first: false,
831                })),
832            },
833            MonotonicityTestCase {
834                name: "asin_order out of domain",
835                func: asin_order,
836                lower: -2.0,
837                upper: 1.0,
838                input_sort: SortProperties::Ordered(SortOptions {
839                    descending: false,
840                    nulls_first: false,
841                }),
842                expected: exec_err!("Input range of ASIN contains out-of-domain values"),
843            },
844            MonotonicityTestCase {
845                name: "asinh_order within domain",
846                func: asinh_order,
847                lower: -1.0,
848                upper: 1.0,
849                input_sort: SortProperties::Ordered(SortOptions {
850                    descending: false,
851                    nulls_first: false,
852                }),
853                expected: Ok(SortProperties::Ordered(SortOptions {
854                    descending: false,
855                    nulls_first: false,
856                })),
857            },
858            MonotonicityTestCase {
859                name: "asinh_order out of domain",
860                func: asinh_order,
861                lower: -2.0,
862                upper: 1.0,
863                input_sort: SortProperties::Ordered(SortOptions {
864                    descending: false,
865                    nulls_first: false,
866                }),
867                expected: Ok(SortProperties::Ordered(SortOptions {
868                    descending: false,
869                    nulls_first: false,
870                })),
871            },
872            MonotonicityTestCase {
873                name: "atan_order within domain",
874                func: atan_order,
875                lower: -1.0,
876                upper: 1.0,
877                input_sort: SortProperties::Ordered(SortOptions {
878                    descending: false,
879                    nulls_first: false,
880                }),
881                expected: Ok(SortProperties::Ordered(SortOptions {
882                    descending: false,
883                    nulls_first: false,
884                })),
885            },
886            MonotonicityTestCase {
887                name: "atan_order out of domain",
888                func: atan_order,
889                lower: -2.0,
890                upper: 1.0,
891                input_sort: SortProperties::Ordered(SortOptions {
892                    descending: false,
893                    nulls_first: false,
894                }),
895                expected: Ok(SortProperties::Ordered(SortOptions {
896                    descending: false,
897                    nulls_first: false,
898                })),
899            },
900            MonotonicityTestCase {
901                name: "atanh_order within domain",
902                func: atanh_order,
903                lower: -0.6,
904                upper: 0.6,
905                input_sort: SortProperties::Ordered(SortOptions {
906                    descending: false,
907                    nulls_first: false,
908                }),
909                expected: Ok(SortProperties::Ordered(SortOptions {
910                    descending: false,
911                    nulls_first: false,
912                })),
913            },
914            MonotonicityTestCase {
915                name: "atanh_order out of domain",
916                func: atanh_order,
917                lower: -2.0,
918                upper: 1.0,
919                input_sort: SortProperties::Ordered(SortOptions {
920                    descending: false,
921                    nulls_first: false,
922                }),
923                expected: exec_err!("Input range of ATANH contains out-of-domain values"),
924            },
925            MonotonicityTestCase {
926                name: "cbrt_order within domain",
927                func: cbrt_order,
928                lower: -1.0,
929                upper: 1.0,
930                input_sort: SortProperties::Ordered(SortOptions {
931                    descending: false,
932                    nulls_first: false,
933                }),
934                expected: Ok(SortProperties::Ordered(SortOptions {
935                    descending: false,
936                    nulls_first: false,
937                })),
938            },
939            MonotonicityTestCase {
940                name: "cbrt_order out of domain",
941                func: cbrt_order,
942                lower: -2.0,
943                upper: 1.0,
944                input_sort: SortProperties::Ordered(SortOptions {
945                    descending: false,
946                    nulls_first: false,
947                }),
948                expected: Ok(SortProperties::Ordered(SortOptions {
949                    descending: false,
950                    nulls_first: false,
951                })),
952            },
953            MonotonicityTestCase {
954                name: "ceil_order within domain",
955                func: ceil_order,
956                lower: -1.0,
957                upper: 1.0,
958                input_sort: SortProperties::Ordered(SortOptions {
959                    descending: false,
960                    nulls_first: false,
961                }),
962                expected: Ok(SortProperties::Ordered(SortOptions {
963                    descending: false,
964                    nulls_first: false,
965                })),
966            },
967            MonotonicityTestCase {
968                name: "ceil_order out of domain",
969                func: ceil_order,
970                lower: -2.0,
971                upper: 1.0,
972                input_sort: SortProperties::Ordered(SortOptions {
973                    descending: false,
974                    nulls_first: false,
975                }),
976                expected: Ok(SortProperties::Ordered(SortOptions {
977                    descending: false,
978                    nulls_first: false,
979                })),
980            },
981            MonotonicityTestCase {
982                name: "cos_order within domain",
983                func: cos_order,
984                lower: 0.0,
985                upper: 2.0 * std::f64::consts::PI,
986                input_sort: SortProperties::Ordered(SortOptions {
987                    descending: false,
988                    nulls_first: false,
989                }),
990                expected: Ok(SortProperties::Unordered),
991            },
992            MonotonicityTestCase {
993                name: "cos_order out of domain",
994                func: cos_order,
995                lower: -2.0,
996                upper: 1.0,
997                input_sort: SortProperties::Ordered(SortOptions {
998                    descending: false,
999                    nulls_first: false,
1000                }),
1001                expected: Ok(SortProperties::Unordered),
1002            },
1003            MonotonicityTestCase {
1004                name: "cosh_order within domain positive",
1005                func: cosh_order,
1006                lower: 5.0,
1007                upper: 100.0,
1008                input_sort: SortProperties::Ordered(SortOptions {
1009                    descending: false,
1010                    nulls_first: false,
1011                }),
1012                expected: Ok(SortProperties::Ordered(SortOptions {
1013                    descending: false,
1014                    nulls_first: false,
1015                })),
1016            },
1017            MonotonicityTestCase {
1018                name: "cosh_order within domain negative",
1019                func: cosh_order,
1020                lower: -100.0,
1021                upper: -5.0,
1022                input_sort: SortProperties::Ordered(SortOptions {
1023                    descending: false,
1024                    nulls_first: false,
1025                }),
1026                expected: Ok(SortProperties::Ordered(SortOptions {
1027                    descending: true,
1028                    nulls_first: false,
1029                })),
1030            },
1031            MonotonicityTestCase {
1032                name: "cosh_order out of domain so unordered",
1033                func: cosh_order,
1034                lower: -1.0,
1035                upper: 1.0,
1036                input_sort: SortProperties::Ordered(SortOptions {
1037                    descending: false,
1038                    nulls_first: false,
1039                }),
1040                expected: Ok(SortProperties::Unordered),
1041            },
1042            MonotonicityTestCase {
1043                name: "degrees_order",
1044                func: degrees_order,
1045                lower: -1.0,
1046                upper: 1.0,
1047                input_sort: SortProperties::Ordered(SortOptions {
1048                    descending: true,
1049                    nulls_first: true,
1050                }),
1051                expected: Ok(SortProperties::Ordered(SortOptions {
1052                    descending: true,
1053                    nulls_first: true,
1054                })),
1055            },
1056            MonotonicityTestCase {
1057                name: "exp_order",
1058                func: exp_order,
1059                lower: -1000.0,
1060                upper: 1000.0,
1061                input_sort: SortProperties::Ordered(SortOptions {
1062                    descending: false,
1063                    nulls_first: false,
1064                }),
1065                expected: Ok(SortProperties::Ordered(SortOptions {
1066                    descending: false,
1067                    nulls_first: false,
1068                })),
1069            },
1070            MonotonicityTestCase {
1071                name: "floor_order",
1072                func: floor_order,
1073                lower: -1.0,
1074                upper: 1.0,
1075                input_sort: SortProperties::Ordered(SortOptions {
1076                    descending: true,
1077                    nulls_first: true,
1078                }),
1079                expected: Ok(SortProperties::Ordered(SortOptions {
1080                    descending: true,
1081                    nulls_first: true,
1082                })),
1083            },
1084            MonotonicityTestCase {
1085                name: "ln_order within domain",
1086                func: ln_order,
1087                lower: 1.0,
1088                upper: 2.0,
1089                input_sort: SortProperties::Ordered(SortOptions {
1090                    descending: false,
1091                    nulls_first: false,
1092                }),
1093                expected: Ok(SortProperties::Ordered(SortOptions {
1094                    descending: false,
1095                    nulls_first: false,
1096                })),
1097            },
1098            MonotonicityTestCase {
1099                name: "ln_order out of domain",
1100                func: ln_order,
1101                lower: -5.0,
1102                upper: -4.0,
1103                input_sort: SortProperties::Ordered(SortOptions {
1104                    descending: false,
1105                    nulls_first: false,
1106                }),
1107                expected: exec_err!("Input range of LN contains out-of-domain values"),
1108            },
1109        ];
1110
1111        for tcase in test_cases {
1112            let input = vec![create_ep(tcase.lower, tcase.upper, tcase.input_sort)];
1113            let actual = (tcase.func)(&input);
1114            match (&actual, &tcase.expected) {
1115                (Ok(a), Ok(e)) => assert_eq!(
1116                    a, e,
1117                    "Test '{}' failed: got {:?}, expected {:?}",
1118                    tcase.name, a, e
1119                ),
1120                (Err(e1), Err(e2)) => {
1121                    assert_eq!(
1122                        e1.strip_backtrace().to_string(),
1123                        e2.strip_backtrace().to_string(),
1124                        "Test '{}' failed: got {:?}, expected {:?}",
1125                        tcase.name,
1126                        e1,
1127                        e2
1128                    )
1129                } // Both are errors, so it's fine
1130                _ => panic!(
1131                    "Test '{}' failed: got {:?}, expected {:?}",
1132                    tcase.name, actual, tcase.expected
1133                ),
1134            }
1135        }
1136    }
1137}