Skip to content

Locale-invariant parsing and formatting #251

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 105 commits into from
Closed
Show file tree
Hide file tree
Changes from 92 commits
Commits
Show all changes
105 commits
Select commit Hold shift + click to select a range
e517b81
WIP: first draft of locale-invariant parsing and formatting
dkhalanskyjb Feb 26, 2023
9bb6ca8
Refactor: reify the string builder directives
dkhalanskyjb Mar 15, 2023
324fd58
Refactoring: move the parser-combining logic to `Parser`
dkhalanskyjb Mar 20, 2023
99a05a2
Fix parsing of years exceeding 4 digits
dkhalanskyjb Mar 21, 2023
93391c2
Add some tests for common formats
dkhalanskyjb Mar 21, 2023
aab6f23
Fix UtcOffset parsing
dkhalanskyjb Mar 22, 2023
273031f
Fix using complex string formats on Kotlin/Native
dkhalanskyjb Mar 22, 2023
2cb10c4
Support LocalDateFormat.appendMonthName
dkhalanskyjb Mar 27, 2023
d78fdb1
Implement the Unicode format strings
dkhalanskyjb Mar 30, 2023
ab60f61
Implement 12-hour clock formats
dkhalanskyjb Apr 13, 2023
6df67fc
Implement == for ValueBag
dkhalanskyjb Apr 13, 2023
444d310
Implement formats with day of the week
dkhalanskyjb Apr 13, 2023
0c5f27d
Implement the RFC 1123 format
dkhalanskyjb Apr 13, 2023
cc35b13
Redo the model of numeric signs to support multi-field values
dkhalanskyjb Apr 14, 2023
7186e33
Work around some JS bug
dkhalanskyjb Apr 14, 2023
35d867f
Implement pretty-printing for formatters
dkhalanskyjb Apr 25, 2023
aa76138
Rework the parser infrastructure for performance a bit
dkhalanskyjb Apr 25, 2023
ddb90aa
Introduce a common interface for date/time formats
dkhalanskyjb Apr 27, 2023
9f44bc7
Support `find` and `findAll` on formats
dkhalanskyjb Apr 27, 2023
2225bf2
Make "-" a non-special character in format strings
dkhalanskyjb Apr 27, 2023
fe1f03e
Refactor: gather all code for string representation of formatters
dkhalanskyjb May 8, 2023
92c966f
Small refactoring
dkhalanskyjb May 17, 2023
c058b81
Introduce changes due to the first round of reviews
dkhalanskyjb Jun 9, 2023
d5ed029
Create entry points for formatting API like `LocalDate.Format.build`
dkhalanskyjb Jun 9, 2023
dc2098d
Implement upper bounds for ValueBag values
dkhalanskyjb Jul 11, 2023
dee428a
Remove the no-longer-needed code for the LRU cache
dkhalanskyjb Jul 11, 2023
7c3096d
Remove the find and findAll functions
dkhalanskyjb Jul 11, 2023
1158788
Add formatTo and parseOrNull
dkhalanskyjb Jul 11, 2023
1161367
Add tests for assigning out-of-bounds values to ValueBag
dkhalanskyjb Jul 11, 2023
7711596
Add tests for error handling on parsing
dkhalanskyjb Jul 11, 2023
51e0c8a
Implement month and day-of-week name classes
dkhalanskyjb Jul 11, 2023
0a46e36
Implement formats for two-digit year
dkhalanskyjb Jul 12, 2023
c05e34b
Reimplement padding as space/zero/none
dkhalanskyjb Jul 14, 2023
2e23dab
Make the UTC offset hour contain the sign
dkhalanskyjb Jul 14, 2023
a9cd48b
*.Format.build { } -> *.Format { }
dkhalanskyjb Jul 14, 2023
1849bb7
Disable configuring whether the year is output with a sign on padding…
dkhalanskyjb Jul 14, 2023
49a277a
Refactoring to clear up the internal models
dkhalanskyjb Jul 14, 2023
1b96711
Replace a single 'appendAlternatives' with clearer, orthogonal 'alter…
dkhalanskyjb Jul 15, 2023
3450c85
Address the review
dkhalanskyjb Jul 24, 2023
6cd875b
Work around a bug
dkhalanskyjb Jul 24, 2023
37ad875
Implement predefined constants for popular formats
dkhalanskyjb Jul 26, 2023
a845d81
Clarify the semantics of formatting fractional values
dkhalanskyjb Jul 26, 2023
b0a076e
Make all the fields in a ValueBag independent from one another
dkhalanskyjb Jul 27, 2023
86d912a
Implement appending whole formats in builders
dkhalanskyjb Jul 27, 2023
d049afc
Document everything
dkhalanskyjb Jul 28, 2023
a73e4aa
Add tests for timezone ID parsing and formatting
dkhalanskyjb Jul 28, 2023
2529d49
Fix the code representation of the UTC offset hour
dkhalanskyjb Jul 28, 2023
f78a90d
Add a missing @SharedImmutable
dkhalanskyjb Jul 28, 2023
ac5d388
Typo: toLocaldate -> toLocalDate
dkhalanskyjb Sep 8, 2023
cd0841d
First stage of renaming
dkhalanskyjb Sep 8, 2023
81a660f
Reorganize the entry points for the formatting API
dkhalanskyjb Sep 8, 2023
4185fe8
Refactoring: remove a redundant class
dkhalanskyjb Sep 11, 2023
e9ec023
Second stage of renaming
dkhalanskyjb Sep 18, 2023
3b573ea
Fixups
dkhalanskyjb Sep 20, 2023
5d38ead
Remove a redundant annotation
dkhalanskyjb Oct 17, 2023
f3a9980
appendLiteral -> chars, char
dkhalanskyjb Oct 24, 2023
f751b9f
Add an OptIn for appendUnicodeFormatString
dkhalanskyjb Oct 24, 2023
905b453
appendOptional -> optional
dkhalanskyjb Oct 24, 2023
ae1d624
Rename the parameters of alternativeParsing
dkhalanskyjb Oct 24, 2023
97e8ff9
Reorganize the hierarchy of builders
dkhalanskyjb Oct 24, 2023
d8a1396
ValueBag -> DateTimeComponents
dkhalanskyjb Oct 24, 2023
1ec4777
Implement formatAsKotlinBuilderDsl properly
dkhalanskyjb Oct 24, 2023
edad7f4
Print seconds in ISO constants for time and datetime
dkhalanskyjb Oct 24, 2023
19a03a8
Rename DateTimeComponents functions
dkhalanskyjb Oct 24, 2023
1a9ec34
Work around a segfault
dkhalanskyjb Oct 24, 2023
0a0cf6c
Performance improvements
dkhalanskyjb Nov 9, 2023
da45897
Introduce benchmarks module with basic formatting benchmark
qwwdfsad Nov 9, 2023
acbb44d
Add README.md for benchmarks
qwwdfsad Nov 9, 2023
603a483
Rename the remaining functions
dkhalanskyjb Nov 14, 2023
288e976
Ensure that `optional` sets the fields to their default values
dkhalanskyjb Nov 14, 2023
c9c609d
Rework parsing to ensure a specific traversal order
dkhalanskyjb Nov 15, 2023
90c9d37
Avoid copying in branches that never get entered
dkhalanskyjb Nov 15, 2023
1d391f8
Hide the functionality to add extra zeros to the second's fraction
dkhalanskyjb Nov 16, 2023
f269b7a
Refactor the builder code
dkhalanskyjb Nov 16, 2023
21a511d
Refactor
dkhalanskyjb Nov 16, 2023
59fd382
Add tests for the Unicode patterns
dkhalanskyjb Nov 16, 2023
e5ff04e
Remove an unnecessary intermediate class
dkhalanskyjb Nov 16, 2023
4047c07
Update the docs for byUnicodePattern
dkhalanskyjb Nov 16, 2023
528aafe
Improve the error messages for incompatible Unicode directives
dkhalanskyjb Nov 16, 2023
7b73212
Ensure that the value-is-reassigned error is not an exception
dkhalanskyjb Nov 17, 2023
40542f5
Final touches to the docs
dkhalanskyjb Nov 17, 2023
0af8ed5
Test that 60 seconds and 24 hours are not parsed for Instant
dkhalanskyjb Nov 29, 2023
f64f120
Adapt the code to the new Kotlin version
dkhalanskyjb Dec 5, 2023
6bed380
Refactor a bit
dkhalanskyjb Dec 5, 2023
d6301ca
Remove the parser from Native
dkhalanskyjb Dec 5, 2023
5bd918f
Add diagnostic messages
dkhalanskyjb Dec 5, 2023
84e7d81
Support string constants that begin or end with numbers
dkhalanskyjb Dec 5, 2023
122dd9c
Address the review
dkhalanskyjb Dec 18, 2023
db723fa
Address more review points
dkhalanskyjb Dec 18, 2023
d552935
Do not add trailing zeros to the fractions of seconds in standard for…
dkhalanskyjb Dec 19, 2023
a13e228
Mention the overloads of secondFraction() in the docs
dkhalanskyjb Jan 15, 2024
24d8ae8
Don't support DecimalFraction.hashCode
dkhalanskyjb Jan 15, 2024
3f5095b
Convert `y` to `u` automatically, but emit a comment about it
dkhalanskyjb Jan 18, 2024
853ddc7
Mention the rounding mode in `secondFraction` docs
dkhalanskyjb Jan 18, 2024
1ce9d14
Check that we can replicate java.time.Instant.parse
dkhalanskyjb Jan 18, 2024
b5236ed
Update README to include parsing and formatting
dkhalanskyjb Jan 26, 2024
f07c80e
Remove String.toSomething
dkhalanskyjb Jan 26, 2024
70fb30c
Only allow the ISO extended format in `UtcOffset.parse`
dkhalanskyjb Feb 1, 2024
7f73ac2
Use a single `parse` overload with a default parameter
dkhalanskyjb Feb 1, 2024
a6350ac
Fix a bug in byUnicodePattern that made optional sections mandatory
dkhalanskyjb Feb 14, 2024
071867d
Fix how `formatAsKotlinBuilderDsl` formats pre-defined formats.
dkhalanskyjb Feb 14, 2024
7d2a8fc
Add byUnicodePattern instructions to the README
dkhalanskyjb Feb 14, 2024
e270480
Change 'secondFraction' to truncate instead of rounding
dkhalanskyjb Feb 19, 2024
825ccde
Make ISO_DATE_TIME_OFFSET more consistent with other ISO formats, doc…
dkhalanskyjb Feb 19, 2024
e0598c1
Refactor a test
dkhalanskyjb Feb 19, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@
*.iml
target
build
/local.properties
/local.properties
benchmarks.jar
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -388,3 +388,5 @@ For local builds, you can use a later version of JDK if you don't have that
version installed. Specify the version of this JDK with the `java.mainToolchainVersion` Gradle property.

After that, the project can be opened in IDEA and built with Gradle.

For building and running benchmarks, see [README.md](benchmarks/README.md)
28 changes: 28 additions & 0 deletions benchmarks/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
### Benchmarks utility module

Module that provides benchmarking infrastructure for kotlinx-datetime.
Please note that these benchmarks are typically written with the specific target, hypothesis and effect in mind.

They provide numbers, not insights, and shouldn't be used as the generic comparison and statements like
"X implementaiton or format is faster/slower than Y"


#### Usage

```
// Build `benchmarks.jar` into the project's root
./gradlew :benchmarks:jmhJar

// Run all benchmarks
java -jar benchmarks.jar

// Run dedicated benchmark(s)
java -jar benchmarks.jar Formatting
java -jar benchmarks.jar FormattingBenchmark.formatIso

// Run with the specified number of warmup iterations, measurement iterations, timeunit and mode
java -jar benchmarks.jar -wi 5 -i 5 -tu us -bm thrpt Formatting

// Extensive help
java -jar benchmarks.jar -help
```
34 changes: 34 additions & 0 deletions benchmarks/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright 2019-2023 JetBrains s.r.o. and contributors.
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
*/

plugins {
id("kotlin")
id("me.champeau.jmh")
}


val mainJavaToolchainVersion by ext(project.property("java.mainToolchainVersion"))
val modularJavaToolchainVersion by ext(project.property("java.modularToolchainVersion"))

sourceSets {
dependencies {
implementation(project(":kotlinx-datetime"))
implementation("org.openjdk.jmh:jmh-core:1.35")
}
}

// Publish benchmarks to the root for the easier 'java -jar benchmarks.jar`
tasks.named<Jar>("jmhJar") {
val nullString: String? = null
archiveBaseName.set("benchmarks")
archiveClassifier.set(nullString)
archiveVersion.set(nullString)
archiveVersion.convention(nullString)
destinationDirectory.set(file("$rootDir"))
}

repositories {
mavenCentral()
}
27 changes: 27 additions & 0 deletions benchmarks/src/jmh/kotlin/FormattingBenchmark.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright 2019-2023 JetBrains s.r.o. and contributors.
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
*/

package kotlinx.datetime

import org.openjdk.jmh.annotations.*
import java.util.concurrent.*

@Warmup(iterations = 5, time = 1)
@Measurement(iterations = 5, time = 1)
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@State(Scope.Benchmark)
@Fork(1)
open class FormattingBenchmark {

private val localDateTime = LocalDateTime(2023, 11, 9, 12, 21, 31, 41)
private val formatted = LocalDateTime.Formats.ISO.format(localDateTime)

@Benchmark
fun formatIso() = LocalDateTime.Formats.ISO.format(localDateTime)

@Benchmark
fun parseIso() = LocalDateTime.Formats.ISO.parse(formatted)
Comment on lines +22 to +26
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it useful to commit the benchmark set in this state?
Also, I'd prefer if we dogfood kotlinx-benchmark here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do run them occasionally.

kotlinx-benchmark

Maybe we could do that, but I think it's a problem for after we've published a release with this PR.

Copy link

@lppedd lppedd Jan 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just know that kotlinx-benchmark is not ready outside of JVM. Too many issues still, especially in JS.
But I'd be happy to see it adopted here, it may speed up the resolution of such issues.

}
63 changes: 45 additions & 18 deletions core/common/src/Instant.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

package kotlinx.datetime

import kotlinx.datetime.format.*
import kotlinx.datetime.internal.*
import kotlinx.datetime.serializers.InstantIso8601Serializer
import kotlinx.serialization.Serializable
Expand Down Expand Up @@ -115,6 +116,9 @@ public expect class Instant : Comparable<Instant> {
* where the component for seconds is 60, and for any day, it's possible to observe 23:59:59.
*
* @see Instant.parse
* @see DateTimeComponents.Formats.ISO_DATE_TIME_OFFSET for a very similar format. The difference is that
* [DateTimeComponents.Formats.ISO_DATE_TIME_OFFSET] will not add trailing zeros for readability to the
* fractional part of the second.
*/
public override fun toString(): String

Expand Down Expand Up @@ -149,28 +153,26 @@ public expect class Instant : Comparable<Instant> {
public fun fromEpochSeconds(epochSeconds: Long, nanosecondAdjustment: Int): Instant

/**
* Parses a string that represents an instant in ISO-8601 format including date and time components and
* the mandatory time zone offset and returns the parsed [Instant] value.
* A shortcut for calling [parse] with [DateTimeComponents.Formats.ISO_DATE_TIME_OFFSET].
*
* Supports the following ways of specifying the time zone offset:
* - the `Z` designator for the UTC+0 time zone,
* - a custom time zone offset specified with `+hh`, or `+hh:mm`, or `+hh:mm:ss`
* (with `+` being replaced with `-` for the negative offsets)
* Parses a string that represents an instant in ISO 8601 format including date and time components and
* the mandatory time zone offset and returns the parsed [Instant] value.
*
* Examples of instants in the ISO-8601 format:
* Examples of instants in the ISO 8601 format:
* - `2020-08-30T18:43:00Z`
* - `2020-08-30T18:43:00.500Z`
* - `2020-08-30T18:43:00.50Z`
* - `2020-08-30T18:43:00.123456789Z`
* - `2020-08-30T18:40:00+03:00`
* - `2020-08-30T18:40:00+03:30:20`
* * `2020-01-01T23:59:59.123456789+01`
* * `+12020-01-31T23:59:59Z`
*
* The string is considered to represent time on the UTC-SLS time scale instead of UTC.
* In practice, this means that, even if there is a leap second on the given day, it will not affect how the
* time is parsed, even if it's in the last 1000 seconds of the day.
* Instead, even if there is a negative leap second on the given day, 23:59:59 is still considered valid time.
* 23:59:60 is invalid on UTC-SLS, so parsing it will fail.
* Guaranteed to parse all strings that [Instant.toString] produces.
*
* @throws IllegalArgumentException if the text cannot be parsed or the boundaries of [Instant] are exceeded.
*
* @see Instant.toString
* @see DateTimeComponents.Formats.ISO_DATE_TIME_OFFSET
*/
public fun parse(isoString: String): Instant

Expand Down Expand Up @@ -512,12 +514,37 @@ public fun Instant.minus(other: Instant, unit: DateTimeUnit, timeZone: TimeZone)
public fun Instant.minus(other: Instant, unit: DateTimeUnit.TimeBased): Long =
other.until(this, unit)

internal const val DISTANT_PAST_SECONDS = -3217862419201
internal const val DISTANT_FUTURE_SECONDS = 3093527980800
/**
* Formats this value using the given [format] using the given [offset].
*
* [DateTimeComponents.Formats.ISO_DATE_TIME_OFFSET] is the format very similar to the one used by [toString] and
* [Instant.Companion.parse]. The only difference is that [Instant.toString] adds trailing zeros to the
* fraction-of-second component so that the number of digits after a dot is a multiple of three.
*/
public fun Instant.format(format: DateTimeFormat<DateTimeComponents>, offset: UtcOffset = UtcOffset.ZERO): String {
val instant = this
return format.format { setDateTimeOffset(instant, offset) }
}

/**
* Displays the given Instant in the given [offset].
* Parses an [Instant] value using the given [format].
*
* Equivalent to calling [DateTimeFormat.parse] on [format] with [input] and obtaining the resulting [Instant] using
* [DateTimeComponents.toInstantUsingOffset].
*
* Be careful: this function may throw for some values of the [Instant].
* The string is considered to represent time on the UTC-SLS time scale instead of UTC.
* In practice, this means that, even if there is a leap second on the given day, it will not affect how the
* time is parsed, even if it's in the last 1000 seconds of the day.
* Instead, even if there is a negative leap second on the given day, 23:59:59 is still considered valid time.
* 23:59:60 is invalid on UTC-SLS, so parsing it will fail.
*
* @throws IllegalArgumentException if the text cannot be parsed or the boundaries of [Instant] are exceeded.
*/
internal expect fun Instant.toStringWithOffset(offset: UtcOffset): String
public fun Instant.Companion.parse(input: CharSequence, format: DateTimeFormat<DateTimeComponents>): Instant = try {
format.parse(input).toInstantUsingOffset()
} catch (e: IllegalArgumentException) {
throw DateTimeFormatException("Failed to parse an instant from '$input'", e)
}

internal const val DISTANT_PAST_SECONDS = -3217862419201
internal const val DISTANT_FUTURE_SECONDS = 3093527980800
83 changes: 80 additions & 3 deletions core/common/src/LocalDate.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

package kotlinx.datetime

import kotlinx.datetime.format.*
import kotlinx.datetime.serializers.LocalDateIso8601Serializer
import kotlinx.serialization.Serializable

Expand Down Expand Up @@ -41,10 +42,67 @@ public expect class LocalDate : Comparable<LocalDate> {
*/
public fun fromEpochDays(epochDays: Int): LocalDate

/**
* Creates a new format for parsing and formatting [LocalDate] values.
*
* Example:
* ```
* // 2020 Jan 05
* LocalDate.Format {
* year()
* char(' ')
* monthName(MonthNames.ENGLISH_ABBREVIATED)
* char(' ')
* dayOfMonth()
* }
* ```
*
* Only parsing and formatting of well-formed values is supported. If the input does not fit the boundaries
* (for example, [dayOfMonth] is 31 for February), consider using [DateTimeComponents.Format] instead.
*
* There is a collection of predefined formats in [LocalDate.Formats].
*/
@Suppress("FunctionName")
public fun Format(block: DateTimeFormatBuilder.WithDate.() -> Unit): DateTimeFormat<LocalDate>

internal val MIN: LocalDate
internal val MAX: LocalDate
}

/**
* A collection of predefined formats for parsing and formatting [LocalDate] values.
*
* See [LocalDate.Formats.ISO] and [LocalDate.Formats.ISO_BASIC] for popular predefined formats.
* [LocalDate.parse] and [LocalDate.toString] can be used as convenient shortcuts for the
* [LocalDate.Formats.ISO] format.
*
* If predefined formats are not sufficient, use [LocalDate.Format] to create a custom
* [kotlinx.datetime.format.DateTimeFormat] for [LocalDate] values.
*/
public object Formats {
/**
* ISO 8601 extended format, which is the format used by [LocalDate.toString] and [LocalDate.parse].
*
* Examples of dates in ISO 8601 format:
* - `2020-08-30`
* - `+12020-08-30`
* - `0000-08-30`
* - `-0001-08-30`
*/
public val ISO: DateTimeFormat<LocalDate>

/**
* ISO 8601 basic format.
*
* Examples of dates in ISO 8601 basic format:
* - `20200830`
* - `+120200830`
* - `00000830`
* - `-00010830`
*/
public val ISO_BASIC: DateTimeFormat<LocalDate>
}

/**
* Constructs a [LocalDate] instance from the given date components.
*
Expand Down Expand Up @@ -77,14 +135,19 @@ public expect class LocalDate : Comparable<LocalDate> {

/** Returns the year component of the date. */
public val year: Int

/** Returns the number-of-month (1..12) component of the date. */
public val monthNumber: Int

/** Returns the month ([Month]) component of the date. */
public val month: Month

/** Returns the day-of-month component of the date. */
public val dayOfMonth: Int

/** Returns the day-of-week component of the date. */
public val dayOfWeek: DayOfWeek

/** Returns the day-of-year component of the date. */
public val dayOfYear: Int

Expand Down Expand Up @@ -113,6 +176,21 @@ public expect class LocalDate : Comparable<LocalDate> {
public override fun toString(): String
}

/**
* Formats this value using the given [format].
* Equivalent to calling [DateTimeFormat.format] on [format] with `this`.
*/
public fun LocalDate.format(format: DateTimeFormat<LocalDate>): String = format.format(this)

/**
* Parses a [LocalDate] value using the given [format].
* Equivalent to calling [DateTimeFormat.parse] on [format] with [input].
*
* @throws IllegalArgumentException if the text cannot be parsed or the boundaries of [LocalDate] are exceeded.
*/
public fun LocalDate.Companion.parse(input: CharSequence, format: DateTimeFormat<LocalDate>): LocalDate =
format.parse(input)

/**
* Converts this string representing a date in ISO-8601 format to a [LocalDate] value.
*
Expand Down Expand Up @@ -162,9 +240,8 @@ public operator fun LocalDate.minus(period: DatePeriod): LocalDate =
if (period.days != Int.MIN_VALUE && period.months != Int.MIN_VALUE) {
plus(with(period) { DatePeriod(-years, -months, -days) })
} else {
minus(period.years, DateTimeUnit.YEAR).
minus(period.months, DateTimeUnit.MONTH).
minus(period.days, DateTimeUnit.DAY)
minus(period.years, DateTimeUnit.YEAR).minus(period.months, DateTimeUnit.MONTH)
.minus(period.days, DateTimeUnit.DAY)
}

/**
Expand Down
Loading