mulesoft-labs / data-weave-rfc

RFC for the data weave language
12 stars 5 forks source link

New additions to dw::core::Periods #19

Open machaval opened 4 years ago

machaval commented 4 years ago

After discussing that dates manipulation is hard we started working on a way to create Periods. I would like to start discussing the new additions

dw::core::Periods

/**
* This module contains functions for working and creating Periods
*/
%dw 2.0

/**
*
* Returns a Period consisting of the number of years, months,
* and days between two dates.
* The start date is included, but the end date is not.
* The period is calculated by removing complete months, then calculating
* the remaining number of days, adjusting to ensure that both have the same sign.
* The number of months is then split into years and months based on a 12 month year.
* A month is considered if the end day-of-month is greater than or equal to the start day-of-month.
* For example, from `2010-01-15` to `2011-03-18` is one year, two months and three days.
* The result of this method can be a negative period if the end is before the start.
*
*  _Introduced in DataWeave 2.3.1. Supported by Mule 4.3.1 and later._
*
* === Parameters
*
* [%header, cols="1,3"]
* |===
* | Name   | Description
* | startDateInclusive | the start date, inclusive.
* | endDateExclusive | the end date, exclusive.
* |===
*
* === Example
*
* This example shows how the `between` behaves under different inputs.
*
* ==== Source
*
* [source,DataWeave,linenums]
* ----
* import * from dw::core::Periods
* output application/json
* ---
* {
*   a: between(|2010-12-12|,|2010-12-10|),
*   b: between(|2010-12-10|,|2011-12-11|)
* }
*
* ----
*
* ==== Output
*
* [source,Json,linenums]
* ----
* {
*    "a": "P2D",
*    "b": "P-1Y-1D"
*  }
* ----
**/
@Since(version = "2.3.1")
fun between(startDateInclusive: Date, endDateExclusive: Date): Period = native("system::BetweenLocalDateOperator")

/**
* Creates a Duration that represents a time-based amount of time.
* The Duration is build with the given
*  - days : Number of days
*  - hours : Number of hours
*  - minutes : Number of minutes
*  - seconds : Number of seconds
*
*  Any of the given parts can be a decimal number and the corresponding transformation is going to be done.
*
*  _Introduced in DataWeave 2.3.1. Supported by Mule 4.3.1 and later._
*
* === Parameters
*
* [%header, cols="1,3"]
* |===
* | Name   | Description
* | period | The period value to be created
* |===
*
* === Example
*
* This example shows how the `duration` behaves under different inputs.
*
* ==== Source
*
* [source,DataWeave,linenums]
* ----
* %dw 2.0
* import * from dw::core::Periods
* output application/json
* ---
* {
*    yesterday: |2020-10-05T20:22:34.385Z| - duration({days: 1}),
*    dayBeforeHourBefore: |2020-10-05T20:22:34.385Z| - duration({days: 1, hours: 1}),
*    pointInTimeBefore: |2020-10-05T20:22:34.385Z| - duration({days: 1, hours: 1, minutes: 20, seconds: 10})
*  }
* ----
*
* ==== Output
*
* [source,Json,linenums]
* ----
* {
*   "yesterday": "2020-10-04T20:22:34.385Z",
*   "dayBeforeHourBefore": "2020-10-04T19:22:34.385Z",
*   "pointInTimeBefore": "2020-10-04T19:02:24.385Z"
* }
* ----
**/
@Since(version = "2.3.1")
fun duration(period:{ days?: Number, hours?: Number, minutes?: Number, seconds?: Number }): Period =  do {
    var nDays   = period.days default 0
    var days    = floor(nDays)
    var nHours  = period.hours default 0 + ((nDays - days) * 60)
    var hours   = floor(nHours)
    var nMinutes= period.minutes default 0 + ((nHours - hours) * 60)
    var minutes = floor(nMinutes )
    var seconds = period.seconds default 0 + ((nMinutes - minutes) * 60)
    ---
    "P$(days as String {format:'#'})DT$(hours as String {format:'#'})H$(minutes as String {format:'#'})M$(seconds as String)S" as Period
}

/**
* Creates a Period that represents date-based amount of time in the ISO-8601 calendar system, such as '2 years, 3 months and 4 days'.
*
*
*  _Introduced in DataWeave 2.3.1. Supported by Mule 4.3.1 and later._
*
* === Parameters
*
* [%header, cols="1,3"]
* |===
* | Name   | Description
* | period | The period object
* |===
*
* === Example
*
* This example shows how the `period` behaves under different inputs.
*
* ==== Source
*
* [source,DataWeave,linenums]
* ----
* %dw 2.0
* output application/json
* import * from dw::core::Periods
* ---
* {
*   lastYear: |2020-10-05T20:22:34.385Z| - period({years: 1}),
*   "1year1month1dayBefore": |2020-10-05T20:22:34.385Z| - duration({years: 1, months: 1, days: 1})
* }
*
* ----
*
* ==== Output
*
* [source,Json,linenums]
* ----
* {
*    "lastYear": "2019-10-05T20:22:34.385Z",
*    "1year1month1dayBefore": "2019-09-04T20:22:34.385Z"
*  }
* ----
**/
@Since(version = "2.3.1")
fun period(period:{ years?: Number, months?: Number, days?: Number}): Period =  do {
    var years = (period.years default 0)
    var months = (period.years default 0)
    var days = (period.years default 0)
    var nYears = if(isDecimal(years)) dw::Runtime::fail("Field years: `$(years)`, can not be decimal.") else years as String {format: "#"}
    var nMonth = if(isDecimal(months)) dw::Runtime::fail("Field months: `$(months)`, can not be decimal.") else months as String {format: "#"}
    var nDays = if(isDecimal(days)) dw::Runtime::fail("Field days: `$(days)`, can not be decimal.") else days as String {format: "#"}
    ---
    "P$(nYears)Y$(nMonth)M$(nDays)D" as Period
}

/**
* Create a Period that represents a given amount of years, such as `3years`
*
*
*  _Introduced in DataWeave 2.3.1. Supported by Mule 4.3.1 and later._
*
* === Parameters
*
* [%header, cols="1,3"]
* |===
* | Name   | Description
* | nYears | The number of years
* |===
*
* === Example
*
* This example shows how the `years` behaves under different inputs.
*
* ==== Source
*
* [source,DataWeave,linenums]
* ----
* %dw 2.0
* import * from dw::core::Periods
* output application/json
* ---
* {
*   nextYear: |2020-10-05T20:22:34.385Z| + years(1)
* }
*
* ----
*
* ==== Output
*
* [source,Json,linenums]
* ----
* {
*    "nextYear": "2021-10-05T20:22:34.385Z"
* }
* ----
**/
@Since(version = "2.3.1")
fun years(nYears: Number):Period =
  period({years: nYears})

/**
* Create a Period that represents a given amount of months, such as `2months`
*
*
*  _Introduced in DataWeave 2.3.1. Supported by Mule 4.3.1 and later._
*
* === Parameters
*
* [%header, cols="1,3"]
* |===
* | Name   | Description
* | nYears | The number of months
* |===
*
* === Example
*
* This example shows how the `months` behaves under different inputs.
*
* ==== Source
*
* [source,DataWeave,linenums]
* ----
* %dw 2.0
* import * from dw::core::Periods
* output application/json
* ---
* {
*   nextMonth: |2020-10-05T20:22:34.385Z| + months(1)
* }
*
* ----
*
* ==== Output
*
* [source,Json,linenums]
* ----
* {
*   "nextMonth": "2020-11-05T20:22:34.385Z"
* }
* ----
**/
@Since(version = "2.3.1")
fun months(nMonths: Number):Period =
  period({months: nMonths})

/**
* Create a Period that represents a given amount of days, such as `2days`
*
*
*  _Introduced in DataWeave 2.3.1. Supported by Mule 4.3.1 and later._
*
* === Parameters
*
* [%header, cols="1,3"]
* |===
* | Name   | Description
* | nYears | The number of days. It can be a decimal number
* |===
*
* === Example
*
* This example shows how the `days` behaves under different inputs.
*
* ==== Source
*
* [source,DataWeave,linenums]
* ----
* %dw 2.0
* import * from dw::core::Periods
* output application/json
* ---
* {
*   tomorrow: |2020-10-05T20:22:34.385Z| + days(1),
*   yesterday: |2020-10-05T20:22:34.385Z| - days(1)
* }
* ----
*
* ==== Output
*
* [source,Json,linenums]
* ----
* {
*   "tomorrow": "2020-10-06T20:22:34.385Z",
*   "yesterday": "2020-10-04T20:22:34.385Z"
* }
* ----
**/
@Since(version = "2.3.1")
fun days(nDays: Number):Period =
  duration({days: nDays})

/**
* Create a Duration that represents a given amount of hours, such as `4hours`
*
*
*  _Introduced in DataWeave 2.3.1. Supported by Mule 4.3.1 and later._
*
* === Parameters
*
* [%header, cols="1,3"]
* |===
* | Name   | Description
* | nYears | The number of hours.  It can be a decimal number
* |===
*
* === Example
*
* This example shows how the `hours` behaves under different inputs.
*
* ==== Source
*
* [source,DataWeave,linenums]
* ----
* %dw 2.0
* import * from dw::core::Periods
* output application/json
* ---
* {
*   nextHour: |2020-10-05T20:22:34.385Z| + hours(1),
*   previousHour: |2020-10-05T20:22:34.385Z| - hours(1)
* }
* ----
*
* ==== Output
*
* [source,Json,linenums]
* ----
*{
*  "nextHour": "2020-10-05T21:22:34.385Z",
*  "previousHour": "2020-10-05T19:22:34.385Z"
*}
* ----
**/
@Since(version = "2.3.1")
fun hours(nHours: Number):Period =
  duration({hours: nHours})

/**
* Create a Duration that represents a given amount of minutes, such as `1minutes`
*
*
*  _Introduced in DataWeave 2.3.1. Supported by Mule 4.3.1 and later._
*
* === Parameters
*
* [%header, cols="1,3"]
* |===
* | Name   | Description
* | nYears | The number of minutes.  It can be a decimal number
* |===
*
* === Example
*
* This example shows how the `minutes` behaves under different inputs.
*
* ==== Source
*
* [source,DataWeave,linenums]
* ----
* %dw 2.0
* import * from dw::core::Periods
* output application/json
* ---
* {
*   nextMinute: |2020-10-05T20:22:34.385Z| + minutes(1),
*   previousMinute: |2020-10-05T20:22:34.385Z| - minutes(1)
* }
* ----
*
* ==== Output
*
* [source,Json,linenums]
* ----
{
  "nextMinute": "2020-10-05T20:23:34.385Z",
  "previousMinute": "2020-10-05T20:21:34.385Z"
}
* ----
**/
@Since(version = "2.3.1")
fun minutes(nMinutes: Number):Period =
  duration({minutes: nMinutes})

/**
* Create a Duration that represents a given amount of seconds, such as `1second`
*
*
*  _Introduced in DataWeave 2.3.1. Supported by Mule 4.3.1 and later._
*
* === Parameters
*
* [%header, cols="1,3"]
* |===
* | Name   | Description
* | nYears | The number of seconds.  It can be a decimal number
* |===
*
* === Example
*
* This example shows how the `seconds` behaves under different inputs.
*
* ==== Source
*
* [source,DataWeave,linenums]
* ----
* %dw 2.0
* import * from dw::core::Periods
* output application/json
* ---
* {
*   nextSecond: |2020-10-05T20:22:34.385Z| + seconds(1),
*   previousSecond: |2020-10-05T20:22:34.385Z| - seconds(1)
* }
* ----
*
* ==== Output
*
* [source,Json,linenums]
* ----
* {
*   "nextSecond": "2020-10-05T20:22:35.385Z",
*   "previousSecond": "2020-10-05T20:22:33.385Z"
* }
* ----
**/
@Since(version = "2.3.1")
fun seconds(nSecs: Number):Period =
  duration({seconds: nSecs})

Examples

import * from dw::core::Periods
---
{
  yesterday: now() - days(1),
  lastYear: now() - years(1),
  lastYearBis: now() - period({years: 1}),
  anHourAndHalf: now() - hours(1.5),
  anHourAndHalf: now() - duration({hours:1.5}),
}

A duration is a Time Based Period. And a Period is a Date base Period.

Time based does support decimal numbers and the correct calculation should be done. Date based DOESN'T support decimal numbers and it will FAIL.

machaval commented 4 years ago

This is releated from #17

jorgegarciamule commented 4 years ago

I think var nHours = period.hours default 0 + ((nDays - days) * 60) should be var nHours = period.hours default 0 + ((nDays - days) * 24)

gozdy commented 4 years ago

Would it be possible to set a return format for the between function? This would allow the function to return different time formats. Example:

import * from dw::core::Periods
output application/json
---
{
  a: between(|2010-12-12|,|2010-12-10|) // default, returns a period
   b: between(|2010-12-12|,|2010-12-10|, "seconds") // returns difference in seconds
  c:  between(|2010-12-12|,|2010-12-10|, "hours") // returns difference in hours

 }

==== Output

 {
    "a": "P2D",
    "b": 172800,
    "c": 48
  }
jorgegarciamule commented 4 years ago

The idea is to not truncate seconds anywhere to handle milis or even nanos? What does Period support? I'm thinking about problems that can arise about not defining hard limits.

jorgegarciamule commented 4 years ago

Would it be possible to set a return format for the between function? This would allow the function to return different time formats...

Now you can do:

between(|2010-12-12|,|2010-12-10|) as Number {unit: "months"}

Not very intuitive to guess you need to write "months" neither.

The other way can be also to create casting functions like toDays() but you will end with a full list of methods like this one. I'm not sure which is the cleanest way.

machaval commented 4 years ago

Sorry for not explaining between function was introduce because of a backwards comp issue. For date - date operator it used to return a Period and not a Duration. This was very problematic as Duration is the only thing that can be translated to millis o any other unit of time. So we changed date - date to return Period so it is consistent with datetime - datetime etc... but in order to have a migration path we introduce a System property that behaves just as it used to be and we also introduce this function that returns the Period between the two dates. Regarding type conversions we have a separate issue #18 where we should start talking about this kind of things. We know is it not intuitive to do this so we think we should start working on a new way of doing this things.

jorgegarciamule commented 3 years ago

I see we are tackling durations against Time, but what about working with durations to sum or subtract them? Would simplify this scenario https://stackoverflow.com/questions/68958484/how-to-aggregate-data-with-particular-column-using-dataweave-2-0