Skip to content

Commit f926809

Browse files
yaooqinnmaropu
authored andcommitted
[SPARK-29390][SQL] Add the justify_days(), justify_hours() and justif_interval() functions
### What changes were proposed in this pull request? Add 3 interval functions justify_days, justify_hours, justif_interval to support justify interval values ### Why are the changes needed? For feature parity with postgres add three interval functions to justify interval values. justify_days(interval) | interval | Adjust interval so 30-day time periods are represented as months | justify_days(interval '35 days') | 1 mon 5 days -- | -- | -- | -- | -- justify_hours(interval) | interval | Adjust interval so 24-hour time periods are represented as days | justify_hours(interval '27 hours') | 1 day 03:00:00 justify_interval(interval) | interval | Adjust interval using justify_days and justify_hours, with additional sign adjustments | justify_interval(interval '1 mon -1 hour') | 29 days 23:00:00 ### Does this PR introduce any user-facing change? yes. new interval functions are added ### How was this patch tested? add ut Closes apache#26465 from yaooqinn/SPARK-29390. Authored-by: Kent Yao <[email protected]> Signed-off-by: Takeshi Yamamuro <[email protected]>
1 parent 80fbc38 commit f926809

File tree

8 files changed

+344
-74
lines changed

8 files changed

+344
-74
lines changed

sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/FunctionRegistry.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,9 @@ object FunctionRegistry {
420420
expression[MakeDate]("make_date"),
421421
expression[MakeTimestamp]("make_timestamp"),
422422
expression[MakeInterval]("make_interval"),
423+
expression[JustifyDays]("justify_days"),
424+
expression[JustifyHours]("justify_hours"),
425+
expression[JustifyInterval]("justify_interval"),
423426
expression[DatePart]("date_part"),
424427

425428
// collection functions

sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/intervalExpressions.scala

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ package org.apache.spark.sql.catalyst.expressions
1919

2020
import java.util.Locale
2121

22+
import scala.util.control.NonFatal
23+
2224
import org.apache.spark.sql.catalyst.expressions.codegen.{CodegenContext, ExprCode}
2325
import org.apache.spark.sql.catalyst.util.IntervalUtils
2426
import org.apache.spark.sql.catalyst.util.IntervalUtils._
@@ -257,3 +259,69 @@ case class MakeInterval(
257259

258260
override def prettyName: String = "make_interval"
259261
}
262+
263+
abstract class IntervalJustifyLike(
264+
child: Expression,
265+
justify: CalendarInterval => CalendarInterval,
266+
justifyFuncName: String) extends UnaryExpression with ExpectsInputTypes {
267+
override def inputTypes: Seq[AbstractDataType] = Seq(CalendarIntervalType)
268+
269+
override def dataType: DataType = CalendarIntervalType
270+
271+
override def nullSafeEval(input: Any): Any = {
272+
try {
273+
justify(input.asInstanceOf[CalendarInterval])
274+
} catch {
275+
case NonFatal(_) => null
276+
}
277+
}
278+
279+
override protected def doGenCode(ctx: CodegenContext, ev: ExprCode): ExprCode = {
280+
nullSafeCodeGen(ctx, ev, child => {
281+
val iu = IntervalUtils.getClass.getCanonicalName.stripSuffix("$")
282+
s"""
283+
|try {
284+
| ${ev.value} = $iu.$justifyFuncName($child);
285+
|} catch (java.lang.ArithmeticException e) {
286+
| ${ev.isNull} = true;
287+
|}
288+
|""".stripMargin
289+
})
290+
}
291+
292+
override def prettyName: String = justifyFuncName
293+
}
294+
295+
@ExpressionDescription(
296+
usage = "_FUNC_(expr) - Adjust interval so 30-day time periods are represented as months",
297+
examples = """
298+
Examples:
299+
> SELECT _FUNC_(interval '1 month -59 day 25 hour');
300+
-29 days 25 hours
301+
""",
302+
since = "3.0.0")
303+
case class JustifyDays(child: Expression)
304+
extends IntervalJustifyLike(child, justifyDays, "justifyDays")
305+
306+
@ExpressionDescription(
307+
usage = "_FUNC_(expr) - Adjust interval so 24-hour time periods are represented as days",
308+
examples = """
309+
Examples:
310+
> SELECT _FUNC_(interval '1 month -59 day 25 hour');
311+
1 months -57 days -23 hours
312+
""",
313+
since = "3.0.0")
314+
case class JustifyHours(child: Expression)
315+
extends IntervalJustifyLike(child, justifyHours, "justifyHours")
316+
317+
@ExpressionDescription(
318+
usage = "_FUNC_(expr) - Adjust interval using justifyHours and justifyDays, with additional" +
319+
" sign adjustments",
320+
examples = """
321+
Examples:
322+
> SELECT _FUNC_(interval '1 month -59 day 25 hour');
323+
-27 days -23 hours
324+
""",
325+
since = "3.0.0")
326+
case class JustifyInterval(child: Expression)
327+
extends IntervalJustifyLike(child, justifyInterval, "justifyInterval")

sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/util/IntervalUtils.scala

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -637,4 +637,40 @@ object IntervalUtils {
637637

638638
new CalendarInterval(totalMonths, totalDays, micros)
639639
}
640+
641+
/**
642+
* Adjust interval so 30-day time periods are represented as months.
643+
*/
644+
def justifyDays(interval: CalendarInterval): CalendarInterval = {
645+
val monthToDays = interval.months * DAYS_PER_MONTH
646+
val totalDays = monthToDays + interval.days
647+
val months = Math.toIntExact(totalDays / DAYS_PER_MONTH)
648+
val days = totalDays % DAYS_PER_MONTH
649+
new CalendarInterval(months, days.toInt, interval.microseconds)
650+
}
651+
652+
/**
653+
* Adjust interval so 24-hour time periods are represented as days.
654+
*/
655+
def justifyHours(interval: CalendarInterval): CalendarInterval = {
656+
val dayToUs = MICROS_PER_DAY * interval.days
657+
val totalUs = Math.addExact(interval.microseconds, dayToUs)
658+
val days = totalUs / MICROS_PER_DAY
659+
val microseconds = totalUs % MICROS_PER_DAY
660+
new CalendarInterval(interval.months, days.toInt, microseconds)
661+
}
662+
663+
/**
664+
* Adjust interval using justifyHours and justifyDays, with additional sign adjustments.
665+
*/
666+
def justifyInterval(interval: CalendarInterval): CalendarInterval = {
667+
val monthToDays = DAYS_PER_MONTH * interval.months
668+
val dayToUs = Math.multiplyExact(monthToDays + interval.days, MICROS_PER_DAY)
669+
val totalUs = Math.addExact(interval.microseconds, dayToUs)
670+
val microseconds = totalUs % MICROS_PER_DAY
671+
val totalDays = totalUs / MICROS_PER_DAY
672+
val days = totalDays % DAYS_PER_MONTH
673+
val months = totalDays / DAYS_PER_MONTH
674+
new CalendarInterval(months.toInt, days.toInt, microseconds)
675+
}
640676
}

sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/util/IntervalUtilsSuite.scala

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,4 +266,35 @@ class IntervalUtilsSuite extends SparkFunSuite {
266266
assert(e.getMessage.contains("divide by zero"))
267267
}
268268
}
269+
270+
test("justify days") {
271+
assert(justifyDays(fromString("1 month 35 day")) === new CalendarInterval(2, 5, 0))
272+
assert(justifyDays(fromString("-1 month 35 day")) === new CalendarInterval(0, 5, 0))
273+
assert(justifyDays(fromString("1 month -35 day")) === new CalendarInterval(0, -5, 0))
274+
assert(justifyDays(fromString("-1 month -35 day")) === new CalendarInterval(-2, -5, 0))
275+
assert(justifyDays(fromString("-1 month 2 day")) === new CalendarInterval(0, -28, 0))
276+
}
277+
278+
test("justify hours") {
279+
assert(justifyHours(fromString("29 day 25 hour")) ===
280+
new CalendarInterval(0, 30, 1 * MICROS_PER_HOUR))
281+
assert(justifyHours(fromString("29 day -25 hour")) ===
282+
new CalendarInterval(0, 27, 23 * MICROS_PER_HOUR))
283+
assert(justifyHours(fromString("-29 day 25 hour")) ===
284+
new CalendarInterval(0, -27, -23 * MICROS_PER_HOUR))
285+
assert(justifyHours(fromString("-29 day -25 hour")) ===
286+
new CalendarInterval(0, -30, -1 * MICROS_PER_HOUR))
287+
}
288+
289+
test("justify interval") {
290+
assert(justifyInterval(fromString("1 month 29 day 25 hour")) ===
291+
new CalendarInterval(2, 0, 1 * MICROS_PER_HOUR))
292+
assert(justifyInterval(fromString("-1 month 29 day -25 hour")) ===
293+
new CalendarInterval(0, -2, -1 * MICROS_PER_HOUR))
294+
assert(justifyInterval(fromString("1 month -29 day -25 hour")) ===
295+
new CalendarInterval(0, 0, -1 * MICROS_PER_HOUR))
296+
assert(justifyInterval(fromString("-1 month -29 day -25 hour")) ===
297+
new CalendarInterval(-2, 0, -1 * MICROS_PER_HOUR))
298+
intercept[ArithmeticException](justifyInterval(new CalendarInterval(2, 0, Long.MaxValue)))
299+
}
269300
}

sql/core/src/test/resources/sql-tests/inputs/interval.sql

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,17 @@ select cast('+ 1 second' as interval);
7474
select cast('- 1 second' as interval);
7575
select cast('- -1 second' as interval);
7676
select cast('- +1 second' as interval);
77+
78+
-- justify intervals
79+
select justify_days(cast(null as interval));
80+
select justify_hours(cast(null as interval));
81+
select justify_interval(cast(null as interval));
82+
select justify_days(interval '1 month 59 day 25 hour');
83+
select justify_hours(interval '1 month 59 day 25 hour');
84+
select justify_interval(interval '1 month 59 day 25 hour');
85+
select justify_days(interval '1 month -59 day 25 hour');
86+
select justify_hours(interval '1 month -59 day 25 hour');
87+
select justify_interval(interval '1 month -59 day 25 hour');
88+
select justify_days(interval '1 month 59 day -25 hour');
89+
select justify_hours(interval '1 month 59 day -25 hour');
90+
select justify_interval(interval '1 month 59 day -25 hour');

sql/core/src/test/resources/sql-tests/inputs/postgreSQL/interval.sql

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -157,13 +157,11 @@
157157
-- select '100000000y 10mon -1000000000d -100000h -10min -10.000001s ago'::interval;
158158

159159
-- test justify_hours() and justify_days()
160-
-- [SPARK-29390] Add the justify_days(), justify_hours() and justify_interval() functions
161-
-- SELECT justify_hours(interval '6 months 3 days 52 hours 3 minutes 2 seconds') as `6 mons 5 days 4 hours 3 mins 2 seconds`;
162-
-- SELECT justify_days(interval '6 months 36 days 5 hours 4 minutes 3 seconds') as `7 mons 6 days 5 hours 4 mins 3 seconds`;
160+
SELECT justify_hours(interval '6 months 3 days 52 hours 3 minutes 2 seconds') as `6 mons 5 days 4 hours 3 mins 2 seconds`;
161+
SELECT justify_days(interval '6 months 36 days 5 hours 4 minutes 3 seconds') as `7 mons 6 days 5 hours 4 mins 3 seconds`;
163162

164163
-- test justify_interval()
165-
166-
-- SELECT justify_interval(interval '1 month -1 hour') as `1 month -1 hour`;
164+
SELECT justify_interval(interval '1 month -1 hour') as `1 month -1 hour`;
167165

168166
-- test fractional second input, and detection of duplicate units
169167
-- [SPARK-28259] Date/Time Output Styles and Date Order Conventions

sql/core/src/test/resources/sql-tests/results/interval.sql.out

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
-- Automatically generated by SQLQueryTestSuite
2-
-- Number of queries: 45
2+
-- Number of queries: 57
33

44

55
-- !query 0
@@ -362,3 +362,99 @@ select cast('- +1 second' as interval)
362362
struct<CAST(- +1 second AS INTERVAL):interval>
363363
-- !query 44 output
364364
NULL
365+
366+
367+
-- !query 45
368+
select justify_days(cast(null as interval))
369+
-- !query 45 schema
370+
struct<justifyDays(CAST(NULL AS INTERVAL)):interval>
371+
-- !query 45 output
372+
NULL
373+
374+
375+
-- !query 46
376+
select justify_hours(cast(null as interval))
377+
-- !query 46 schema
378+
struct<justifyHours(CAST(NULL AS INTERVAL)):interval>
379+
-- !query 46 output
380+
NULL
381+
382+
383+
-- !query 47
384+
select justify_interval(cast(null as interval))
385+
-- !query 47 schema
386+
struct<justifyInterval(CAST(NULL AS INTERVAL)):interval>
387+
-- !query 47 output
388+
NULL
389+
390+
391+
-- !query 48
392+
select justify_days(interval '1 month 59 day 25 hour')
393+
-- !query 48 schema
394+
struct<justifyDays(1 months 59 days 25 hours):interval>
395+
-- !query 48 output
396+
2 months 29 days 25 hours
397+
398+
399+
-- !query 49
400+
select justify_hours(interval '1 month 59 day 25 hour')
401+
-- !query 49 schema
402+
struct<justifyHours(1 months 59 days 25 hours):interval>
403+
-- !query 49 output
404+
1 months 60 days 1 hours
405+
406+
407+
-- !query 50
408+
select justify_interval(interval '1 month 59 day 25 hour')
409+
-- !query 50 schema
410+
struct<justifyInterval(1 months 59 days 25 hours):interval>
411+
-- !query 50 output
412+
3 months 1 hours
413+
414+
415+
-- !query 51
416+
select justify_days(interval '1 month -59 day 25 hour')
417+
-- !query 51 schema
418+
struct<justifyDays(1 months -59 days 25 hours):interval>
419+
-- !query 51 output
420+
-29 days 25 hours
421+
422+
423+
-- !query 52
424+
select justify_hours(interval '1 month -59 day 25 hour')
425+
-- !query 52 schema
426+
struct<justifyHours(1 months -59 days 25 hours):interval>
427+
-- !query 52 output
428+
1 months -57 days -23 hours
429+
430+
431+
-- !query 53
432+
select justify_interval(interval '1 month -59 day 25 hour')
433+
-- !query 53 schema
434+
struct<justifyInterval(1 months -59 days 25 hours):interval>
435+
-- !query 53 output
436+
-27 days -23 hours
437+
438+
439+
-- !query 54
440+
select justify_days(interval '1 month 59 day -25 hour')
441+
-- !query 54 schema
442+
struct<justifyDays(1 months 59 days -25 hours):interval>
443+
-- !query 54 output
444+
2 months 29 days -25 hours
445+
446+
447+
-- !query 55
448+
select justify_hours(interval '1 month 59 day -25 hour')
449+
-- !query 55 schema
450+
struct<justifyHours(1 months 59 days -25 hours):interval>
451+
-- !query 55 output
452+
1 months 57 days 23 hours
453+
454+
455+
-- !query 56
456+
select justify_interval(interval '1 month 59 day -25 hour')
457+
-- !query 56 schema
458+
struct<justifyInterval(1 months 59 days -25 hours):interval>
459+
-- !query 56 output
460+
2 months 27 days 23 hours

0 commit comments

Comments
 (0)