├── .github ├── release.yaml └── workflows │ ├── deploy-website.yaml │ ├── maven-publish.yaml │ └── pr.yaml ├── .gitignore ├── .idea └── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── LICENSE.txt ├── README.md ├── build.gradle.kts ├── buildSrc ├── build.gradle.kts ├── settings.gradle.kts └── src │ └── main │ └── kotlin │ ├── android-library.gradle.kts │ ├── base-convention.gradle.kts │ ├── mpp-library.gradle.kts │ ├── published-android-library.gradle.kts │ ├── published-library.gradle.kts │ └── published-mpp-library.gradle.kts ├── core ├── MODULE.md ├── build.gradle.kts ├── gradle.properties └── src │ ├── commonMain │ ├── generated │ │ └── io │ │ │ └── islandtime │ │ │ ├── _Conversions.kt │ │ │ ├── _DateProperties.kt │ │ │ ├── internal │ │ │ └── _Constants.kt │ │ │ ├── measures │ │ │ ├── _Centuries.kt │ │ │ ├── _Days.kt │ │ │ ├── _Decades.kt │ │ │ ├── _Hours.kt │ │ │ ├── _Microseconds.kt │ │ │ ├── _Milliseconds.kt │ │ │ ├── _Minutes.kt │ │ │ ├── _Months.kt │ │ │ ├── _Nanoseconds.kt │ │ │ ├── _Seconds.kt │ │ │ ├── _Weeks.kt │ │ │ └── _Years.kt │ │ │ └── ranges │ │ │ ├── _Operators.kt │ │ │ └── _Properties.kt │ └── kotlin │ │ └── io │ │ └── islandtime │ │ ├── Builders.kt │ │ ├── Conversions.kt │ │ ├── Date.kt │ │ ├── DateTime.kt │ │ ├── DateTimeException.kt │ │ ├── DayOfWeek.kt │ │ ├── Instant.kt │ │ ├── IslandTime.kt │ │ ├── Month.kt │ │ ├── OffsetDateTime.kt │ │ ├── OffsetTime.kt │ │ ├── Time.kt │ │ ├── TimeZone.kt │ │ ├── UtcOffset.kt │ │ ├── WeekDate.kt │ │ ├── Year.kt │ │ ├── YearMonth.kt │ │ ├── ZonedDateTime.kt │ │ ├── base │ │ ├── DateTimeField.kt │ │ └── TimePoint.kt │ │ ├── calendar │ │ └── WeekSettings.kt │ │ ├── clock │ │ ├── Clock.kt │ │ ├── FixedClock.kt │ │ ├── Now.kt │ │ ├── SystemClock.kt │ │ └── internal │ │ │ ├── NowImpl.kt │ │ │ └── SystemClockImpl.kt │ │ ├── format │ │ ├── DateTimeTextProvider.kt │ │ ├── NumberStyle.kt │ │ ├── TextStyle.kt │ │ └── TimeZoneTextProvider.kt │ │ ├── internal │ │ ├── Constants.kt │ │ ├── CopyIfChanged.kt │ │ ├── Deprecation.kt │ │ ├── Extensions.kt │ │ ├── Instants.kt │ │ ├── PlatformImpl.kt │ │ ├── RoundImpl.kt │ │ └── WeekNumbers.kt │ │ ├── locale │ │ └── Locale.kt │ │ ├── measures │ │ ├── Duration.kt │ │ ├── Period.kt │ │ ├── TimeUnit.kt │ │ └── internal │ │ │ └── Extensions.kt │ │ ├── operators │ │ ├── Between.kt │ │ ├── Round.kt │ │ ├── RoundDown.kt │ │ ├── RoundUp.kt │ │ ├── StartEnd.kt │ │ ├── Truncation.kt │ │ └── Week.kt │ │ ├── parser │ │ ├── DateTimeFieldBuilders.kt │ │ ├── DateTimeParseException.kt │ │ ├── DateTimeParseResult.kt │ │ ├── DateTimeParser.kt │ │ ├── DateTimeParserDsl.kt │ │ ├── DateTimeParserSettings.kt │ │ ├── DateTimeParsers.kt │ │ ├── GroupedDateTimeParser.kt │ │ └── internal │ │ │ ├── DateTimeParseContext.kt │ │ │ ├── DateTimeParserBuilder.kt │ │ │ ├── GroupedDateTimeParserBuilder.kt │ │ │ ├── ParserBuilders.kt │ │ │ └── Parsers.kt │ │ ├── ranges │ │ ├── Builders.kt │ │ ├── Conversions.kt │ │ ├── DateIterators.kt │ │ ├── DateProgressions.kt │ │ ├── DateRange.kt │ │ ├── DateTimeInterval.kt │ │ ├── InstantInterval.kt │ │ ├── Interval.kt │ │ ├── OffsetDateTimeInterval.kt │ │ ├── TimePointInterval.kt │ │ ├── TimePointIterators.kt │ │ ├── TimePointProgressions.kt │ │ ├── ZonedDateTimeInterval.kt │ │ └── internal │ │ │ ├── Common.kt │ │ │ └── RandomImpl.kt │ │ ├── serialization │ │ └── Serializers.kt │ │ └── zone │ │ └── TimeZoneRules.kt │ ├── commonTest │ └── kotlin │ │ └── io │ │ └── islandtime │ │ ├── BuildersTest.kt │ │ ├── ConversionsTest.kt │ │ ├── DatePropertiesTest.kt │ │ ├── DateTest.kt │ │ ├── DateTimeTest.kt │ │ ├── DayOfWeekTest.kt │ │ ├── InstantTest.kt │ │ ├── IslandTimeTest.kt │ │ ├── MonthTest.kt │ │ ├── OffsetDateTimeTest.kt │ │ ├── OffsetTimeTest.kt │ │ ├── TimeTest.kt │ │ ├── TimeZoneTest.kt │ │ ├── UtcOffsetTest.kt │ │ ├── WeekDateTest.kt │ │ ├── YearMonthTest.kt │ │ ├── YearTest.kt │ │ ├── ZonedDateTimeTest.kt │ │ ├── calendar │ │ └── WeekSettingsTest.kt │ │ ├── clock │ │ ├── ClockTest.kt │ │ └── NowTest.kt │ │ ├── format │ │ ├── DateTimeTextProviderTest.kt │ │ ├── NumberStyleTest.kt │ │ ├── TextStyleTest.kt │ │ └── TimeZoneTextProviderTest.kt │ │ ├── internal │ │ └── ExtensionsTest.kt │ │ ├── measures │ │ ├── DaysTest.kt │ │ ├── DurationTest.kt │ │ ├── HoursTest.kt │ │ ├── MinutesTest.kt │ │ ├── MonthsTest.kt │ │ ├── NanosecondsTest.kt │ │ ├── PeriodTest.kt │ │ └── YearsTest.kt │ │ ├── operators │ │ ├── BetweenTest.kt │ │ ├── RoundDownTest.kt │ │ ├── RoundTest.kt │ │ ├── RoundUpTest.kt │ │ ├── StartEndTest.kt │ │ └── TruncationTest.kt │ │ ├── parser │ │ ├── AnyOfParserTest.kt │ │ ├── CaseSensitivityTest.kt │ │ ├── DateTimeParserTest.kt │ │ ├── DecimalNumberParserTest.kt │ │ ├── GroupedDateTimeParserTest.kt │ │ ├── LocalizedTextParserTest.kt │ │ ├── OptionalParserTest.kt │ │ ├── StringParserTest.kt │ │ ├── WholeNumberParserTest.kt │ │ └── internal │ │ │ └── ParsersTest.kt │ │ ├── ranges │ │ ├── BuildersTest.kt │ │ ├── ConversionsTest.kt │ │ ├── DateDayProgressionTest.kt │ │ ├── DateMonthProgressionTest.kt │ │ ├── DateRangeTest.kt │ │ ├── DateTimeIntervalTest.kt │ │ ├── InstantIntervalTest.kt │ │ ├── InstantProgressionTest.kt │ │ ├── OffsetDateTimeIntervalTest.kt │ │ └── ZonedDateTimeIntervalTest.kt │ │ ├── test │ │ ├── AbstractIslandTimeTest.kt │ │ └── TestData.kt │ │ └── zone │ │ └── PlatformTimeZoneRulesTest.kt │ ├── darwinMain │ └── kotlin │ │ └── io │ │ └── islandtime │ │ ├── PlatformInstant.kt │ │ ├── calendar │ │ └── WeekSettings.kt │ │ ├── clock │ │ └── internal │ │ │ ├── NowImpl.kt │ │ │ └── SystemClockImpl.kt │ │ ├── darwin │ │ └── Conversions.kt │ │ ├── format │ │ ├── DateTimeTextProvider.kt │ │ ├── NumberStyle.kt │ │ └── TimeZoneTextProvider.kt │ │ ├── internal │ │ └── PlatformImpl.kt │ │ ├── locale │ │ └── Locale.kt │ │ └── zone │ │ └── TimeZoneRules.kt │ ├── darwinTest │ └── kotlin │ │ └── io │ │ └── islandtime │ │ ├── darwin │ │ └── ConversionsTest.kt │ │ └── format │ │ ├── DarwinNumberFormatTest.kt │ │ └── DarwinTimeZoneTextProviderTest.kt │ ├── jvmMain │ ├── kotlin │ │ └── io │ │ │ └── islandtime │ │ │ ├── PlatformInstant.kt │ │ │ ├── calendar │ │ │ └── WeekSettings.kt │ │ │ ├── clock │ │ │ └── internal │ │ │ │ ├── Conversions.kt │ │ │ │ ├── NowImpl.kt │ │ │ │ └── SystemClockImpl.kt │ │ │ ├── format │ │ │ ├── JvmDateTimeTextProvider.kt │ │ │ ├── NumberStyle.kt │ │ │ └── TimeZoneTextProvider.kt │ │ │ ├── internal │ │ │ └── PlatformImpl.kt │ │ │ ├── jvm │ │ │ ├── ClockExtensions.kt │ │ │ └── Conversions.kt │ │ │ ├── locale │ │ │ └── Locale.kt │ │ │ └── zone │ │ │ └── JvmTimeZoneRules.kt │ └── resources │ │ └── META-INF │ │ └── proguard │ │ └── islandtime.pro │ └── jvmTest │ ├── java │ └── io │ │ └── islandtime │ │ └── jvm │ │ └── JavaSanityTest.java │ └── kotlin │ └── io │ └── islandtime │ ├── calendar │ └── JvmWeekSettingsTest.kt │ ├── format │ └── JvmTimeZoneTextProviderTest.kt │ ├── internal │ └── PlatformImplTest.kt │ └── jvm │ ├── ClockExtensionsTest.kt │ ├── ConversionsTest.kt │ ├── DateComparisonTest.kt │ └── DateTimeComparisonTest.kt ├── docs ├── CNAME ├── advanced │ └── custom-providers.md ├── assets │ ├── images │ │ └── logo.svg │ ├── stylesheets │ │ └── extra.css │ └── theme │ │ └── partials │ │ └── copyright.html ├── basics │ ├── clocks.md │ ├── dates-and-times.md │ ├── durations.md │ ├── formatting.md │ ├── interop.md │ ├── intervals.md │ ├── overview.md │ ├── parsing.md │ └── serialization.md ├── extensions │ └── parcelize.md ├── getting-started.md └── index.md ├── extensions └── parcelize │ ├── MODULE.md │ ├── build.gradle.kts │ ├── gradle.properties │ └── src │ ├── androidTest │ └── kotlin │ │ ├── DateRangeTest.kt │ │ ├── DateTest.kt │ │ ├── DateTimeIntervalTest.kt │ │ ├── DateTimeTest.kt │ │ ├── DurationTest.kt │ │ ├── InstantIntervalTest.kt │ │ ├── InstantTest.kt │ │ ├── OffsetDateTimeIntervalTest.kt │ │ ├── OffsetDateTimeTest.kt │ │ ├── OffsetTimeTest.kt │ │ ├── PeriodTest.kt │ │ ├── TimeTest.kt │ │ ├── TimeZoneTest.kt │ │ ├── YearMonthTest.kt │ │ ├── ZonedDateTimeIntervalTest.kt │ │ ├── ZonedDateTimeTest.kt │ │ └── test │ │ └── TestParcelable.kt │ └── main │ └── kotlin │ ├── Date.kt │ ├── DateRange.kt │ ├── DateTime.kt │ ├── DateTimeInterval.kt │ ├── Duration.kt │ ├── Instant.kt │ ├── InstantInterval.kt │ ├── OffsetDateTime.kt │ ├── OffsetDateTimeInterval.kt │ ├── OffsetTime.kt │ ├── Period.kt │ ├── Time.kt │ ├── TimeZone.kt │ ├── YearMonth.kt │ ├── ZonedDateTime.kt │ └── ZonedDateTimeInterval.kt ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── integration-test └── serialization │ ├── build.gradle.kts │ └── src │ └── commonTest │ └── kotlin │ └── SerializationTest.kt ├── mkdocs.yml ├── renovate.json ├── settings.gradle.kts └── tools ├── code-generator ├── build.gradle.kts ├── settings.gradle.kts └── src │ └── main │ └── kotlin │ └── io │ └── islandtime │ └── codegen │ ├── Generator.kt │ ├── Main.kt │ ├── descriptions │ ├── DateTimeDescription.kt │ ├── IntervalDescription.kt │ └── TemporalUnitDescription.kt │ ├── dsl │ ├── CodeWriterDsl.kt │ └── KotlinPoetExtensions.kt │ ├── generators │ ├── ConstantsGenerator.kt │ ├── DateConversionsGenerator.kt │ ├── DatePropertiesGenerator.kt │ ├── IntervalOperatorsGenerator.kt │ ├── IntervalPropertiesGenerator.kt │ ├── TemporalUnitGenerator.kt │ └── Utility.kt │ └── util │ └── Extensions.kt └── mkdocs-dokka-plugin ├── build.gradle.kts ├── settings.gradle.kts └── src └── main ├── kotlin ├── MarkdownContent.kt ├── MkdocsDocumentableToPageTranslator.kt ├── MkdocsPlugin.kt └── MkdocsRenderer.kt └── resources └── META-INF └── services └── org.jetbrains.dokka.plugability.DokkaPlugin /.github/release.yaml: -------------------------------------------------------------------------------- 1 | changelog: 2 | categories: 3 | - title: New Features 4 | labels: 5 | - enhancement 6 | - title: API Changes 7 | labels: 8 | - 'api change' 9 | - title: Bug Fixes 10 | labels: 11 | - bug 12 | - title: Dependency Changes 13 | labels: 14 | - dependencies 15 | -------------------------------------------------------------------------------- /.github/workflows/deploy-website.yaml: -------------------------------------------------------------------------------- 1 | name: Deploy website 2 | 3 | on: 4 | release: 5 | types: [published] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | deploy-website: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | 15 | - name: Validate Gradle wrapper 16 | uses: gradle/actions/wrapper-validation@v4 17 | 18 | - name: Configure Java 19 | uses: actions/setup-java@v4 20 | with: 21 | distribution: 'temurin' 22 | java-version: '21' 23 | 24 | - name: Configure Gradle 25 | uses: gradle/actions/setup-gradle@v4 26 | 27 | - name: Configure Python 28 | uses: actions/setup-python@v5 29 | with: 30 | python-version: '3.x' 31 | 32 | - name: Install mkdocs 33 | run: pip install mkdocs-material mkdocs-macros-plugin 34 | 35 | - name: Build API docs 36 | run: ./gradlew dokkaMkdocsMultiModule 37 | 38 | - name: Deploy 39 | run: mkdocs gh-deploy --force 40 | -------------------------------------------------------------------------------- /.github/workflows/maven-publish.yaml: -------------------------------------------------------------------------------- 1 | name: Publish to Maven 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | runs-on: macos-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | 15 | - name: Validate Gradle wrapper 16 | uses: gradle/actions/wrapper-validation@v4 17 | 18 | - name: Configure Java 19 | uses: actions/setup-java@v4 20 | with: 21 | distribution: 'temurin' 22 | java-version: '21' 23 | 24 | - name: Configure Gradle 25 | uses: gradle/actions/setup-gradle@v4 26 | 27 | - name: Cache Konan 28 | uses: actions/cache@v4 29 | with: 30 | path: ~/.konan 31 | key: ${{ runner.os }}-konan-${{ hashFiles('**/libs.versions.toml') }} 32 | restore-keys: | 33 | ${{ runner.os }}-konan- 34 | 35 | - name: Publish 36 | env: 37 | ORG_GRADLE_PROJECT_repositoryUsername: ${{ secrets.ORG_GRADLE_PROJECT_repositoryUsername }} 38 | ORG_GRADLE_PROJECT_repositoryPassword: ${{ secrets.ORG_GRADLE_PROJECT_repositoryPassword }} 39 | ORG_GRADLE_PROJECT_signingKey: ${{ secrets.ORG_GRADLE_PROJECT_signingKey }} 40 | ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.ORG_GRADLE_PROJECT_signingPassword }} 41 | run: | 42 | ./gradlew assemble 43 | ./gradlew publishAllPublicationsToMavenRepository 44 | -------------------------------------------------------------------------------- /.github/workflows/pr.yaml: -------------------------------------------------------------------------------- 1 | name: Test pull request 2 | 3 | on: 4 | pull_request: 5 | paths-ignore: 6 | - 'docs/**' 7 | - '*.md' 8 | 9 | jobs: 10 | build: 11 | strategy: 12 | matrix: 13 | os: [ macos-13, ubuntu-latest ] 14 | runs-on: ${{matrix.os}} 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | 19 | - name: Validate Gradle wrapper 20 | uses: gradle/actions/wrapper-validation@v4 21 | 22 | - name: Enable KVM group permissions 23 | if: matrix.os == 'ubuntu-latest' 24 | run: | 25 | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules 26 | sudo udevadm control --reload-rules 27 | sudo udevadm trigger --name-match=kvm 28 | 29 | - name: Configure Java 30 | uses: actions/setup-java@v4 31 | with: 32 | distribution: 'temurin' 33 | java-version: '21' 34 | 35 | - name: Configure Gradle 36 | uses: gradle/actions/setup-gradle@v4 37 | 38 | - name: Cache Konan 39 | uses: actions/cache@v4 40 | with: 41 | path: ~/.konan 42 | key: ${{ runner.os }}-konan-${{ hashFiles('**/libs.versions.toml') }} 43 | restore-keys: | 44 | ${{ runner.os }}-konan- 45 | 46 | - name: Build and test 47 | if: matrix.os == 'macos-13' 48 | run: ./gradlew build 49 | 50 | - name: Run Android emulator tests 51 | if: matrix.os == 'ubuntu-latest' 52 | uses: reactivecircus/android-emulator-runner@v2 53 | with: 54 | api-level: 24 55 | arch: x86_64 56 | script: ./gradlew connectedCheck 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/.idea/* 2 | !/.idea/codeStyleSettings.xml 3 | !/.idea/codeStyles 4 | .gradle/ 5 | .kotlin/ 6 | build/ 7 | docs/api/ 8 | site/ 9 | *.iml 10 | local.properties 11 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://github.com/erikc5000/island-time/actions/workflows/maven-publish.yaml/badge.svg?event=push)](https://github.com/erikc5000/island-time/actions/workflows/maven-publish.yaml?query=workflow%3APublish) [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.islandtime/core/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.islandtime/core) 2 | 3 | # Island Time 4 | 5 | A Kotlin Multiplatform library for working with dates and times, heavily inspired by the java.time library. 6 | 7 | Features: 8 | - A full set of date-time primitives such as `Date`, `Time`, `DateTime`, `Instant`, and `ZonedDateTime` 9 | - Time zone database support 10 | - Date ranges and time intervals, integrating with Kotlin ranges and progressions 11 | - Read and write strings in ISO formats 12 | - DSL-based definition of custom parsers 13 | - Access localized text for names of months, days of the week, time zones, etc. 14 | - Convenience operators like `date.next(MONDAY)`, `dateTime.startOfWeek`, or `date.week(WeekSettings.systemDefault())` 15 | - Convert to and from platform-specific date-time types 16 | - Works on JVM, Android, iOS, macOS, tvOS, and watchOS 17 | 18 | Notable Limitations: 19 | - No custom format strings (must write platform-specific code to do this) 20 | - No support for JavaScript or non-Apple native platforms 21 | - Only supports the ISO calendar system 22 | 23 | Island Time is still early in development and "moving fast" so to speak. The API is likely to experience changes between minor version increments. 24 | 25 | See the [project website](https://islandtime.io) for more information along with the API reference docs. 26 | 27 | ## Feedback/Contributions 28 | 29 | The goal of this project is not just to port the java.time library over to Kotlin Multiplatform, but to take full advantage of Kotlin language features to create a date-time DSL that feels natural to users of the language and encourages best practices where possible. To that end, any and all feedback would be much appreciated in helping to iron out the API. 30 | 31 | If you're interested in contributing or have ideas on areas that can be improved (there are definitely many right now), please feel free to initiate a dialog by opening design-related issues or submitting pull requests. 32 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.dokka.gradle.DokkaMultiModuleTask 2 | 3 | plugins { 4 | `base-convention` 5 | id("org.jetbrains.dokka") 6 | alias(libs.plugins.kover) 7 | } 8 | 9 | dependencies { 10 | kover(project(":core")) 11 | } 12 | 13 | tasks.register("dokkaMkdocsMultiModule") { 14 | dependencies { 15 | plugins("io.islandtime.gradle:mkdocs-dokka-plugin") 16 | plugins(libs.dokkaGfmTemplateProcessing) 17 | } 18 | 19 | // This is deprecated, but doesn't seem to have a replacement yet 20 | @Suppress("DEPRECATION") 21 | addSubprojectChildTasks("dokkaMkdocsPartial") 22 | outputDirectory = file("$rootDir/docs/api") 23 | } 24 | 25 | tasks.register("codegen") { 26 | dependsOn(gradle.includedBuild("code-generator").task(":run")) 27 | } 28 | -------------------------------------------------------------------------------- /buildSrc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `kotlin-dsl` 3 | } 4 | 5 | dependencies { 6 | implementation(libs.kotlinGradle) 7 | implementation(libs.dokka) 8 | implementation(libs.androidGradle) 9 | } 10 | -------------------------------------------------------------------------------- /buildSrc/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | } 5 | } 6 | 7 | dependencyResolutionManagement { 8 | repositories { 9 | mavenCentral() 10 | google() 11 | } 12 | 13 | versionCatalogs { 14 | create("libs") { 15 | from(files("../gradle/libs.versions.toml")) 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/android-library.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.library") 3 | kotlin("android") 4 | id("base-convention") 5 | } 6 | 7 | android { 8 | compileSdk = 34 9 | 10 | defaultConfig { 11 | minSdk = 21 12 | } 13 | 14 | compileOptions { 15 | sourceCompatibility = JavaVersion.VERSION_11 16 | targetCompatibility = JavaVersion.VERSION_11 17 | } 18 | 19 | buildFeatures { 20 | buildConfig = false 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/base-convention.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.gradle.api.JavaVersion 2 | import org.gradle.api.tasks.compile.JavaCompile 3 | import org.gradle.api.tasks.testing.AbstractTestTask 4 | import org.gradle.api.tasks.testing.logging.TestExceptionFormat 5 | import org.gradle.api.tasks.testing.logging.TestLogEvent 6 | import org.gradle.kotlin.dsl.withType 7 | import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile 8 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 9 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask 10 | 11 | tasks.withType().configureEach { 12 | sourceCompatibility = JavaVersion.VERSION_11.toString() 13 | targetCompatibility = JavaVersion.VERSION_11.toString() 14 | } 15 | 16 | tasks.withType().configureEach { 17 | compilerOptions { 18 | jvmTarget = JvmTarget.JVM_11 19 | } 20 | } 21 | 22 | tasks.withType>().configureEach { 23 | compilerOptions { 24 | freeCompilerArgs.add("-Xexpect-actual-classes") 25 | } 26 | } 27 | 28 | tasks.withType().configureEach { 29 | testLogging { 30 | events = setOf(TestLogEvent.FAILED) 31 | exceptionFormat = TestExceptionFormat.FULL 32 | showStackTraces = true 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/mpp-library.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("multiplatform") 3 | id("base-convention") 4 | } 5 | 6 | kotlin { 7 | jvm() 8 | 9 | val darwinTargets = listOf( 10 | iosArm64(), 11 | iosX64(), 12 | iosSimulatorArm64(), 13 | macosX64(), 14 | macosArm64(), 15 | watchosArm64(), 16 | watchosSimulatorArm64(), 17 | watchosX64(), 18 | tvosArm64(), 19 | tvosX64(), 20 | tvosSimulatorArm64() 21 | ) 22 | 23 | sourceSets { 24 | all { 25 | languageSettings.apply { 26 | optIn("kotlin.RequiresOptIn") 27 | progressiveMode = true 28 | } 29 | } 30 | 31 | val commonMain by getting 32 | val commonTest by getting 33 | 34 | val darwinMain by creating { 35 | dependsOn(commonMain) 36 | } 37 | 38 | val darwinTest by creating { 39 | dependsOn(commonTest) 40 | } 41 | 42 | configure(darwinTargets) { 43 | compilations["main"].defaultSourceSet.dependsOn(darwinMain) 44 | compilations["test"].defaultSourceSet.dependsOn(darwinTest) 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/published-android-library.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("android-library") 3 | id("published-library") 4 | } 5 | 6 | android { 7 | publishing { 8 | singleVariant("release") { 9 | withSourcesJar() 10 | } 11 | } 12 | } 13 | 14 | afterEvaluate { 15 | publishing { 16 | publications { 17 | create("releaseAar") { 18 | from(components["release"]) 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /buildSrc/src/main/kotlin/published-mpp-library.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("mpp-library") 3 | id("published-library") 4 | } 5 | 6 | publishing { 7 | val pomMppArtifactId: String? by project 8 | 9 | publications.withType().configureEach { 10 | if (pomMppArtifactId != null) { 11 | artifactId = if (name == "kotlinMultiplatform") { 12 | pomMppArtifactId 13 | } else { 14 | "${pomMppArtifactId}-$name" 15 | } 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /core/MODULE.md: -------------------------------------------------------------------------------- 1 | # Module core 2 | 3 | The core set of date, time, duration, and interval types, supporting the ISO calendar system. 4 | 5 | # Package io.islandtime 6 | 7 | Date-time primitives and core concepts, including classes such as `Date`, `Time`, `Instant`, and `ZonedDateTime`. 8 | 9 | # Package io.islandtime.base 10 | 11 | *(Experimental)* Framework-level interfaces, allowing aspects of date, time, and duration to be abstracted. This area is unstable and likely to see significant change. 12 | 13 | # Package io.islandtime.calendar 14 | 15 | Platform-independent calendar properties. 16 | 17 | # Package io.islandtime.clock 18 | 19 | The default clock implementation, providing access to the current system time at up to nanosecond precision when available. 20 | 21 | # Package io.islandtime.darwin 22 | 23 | Various extensions specific to the Apple platform. 24 | 25 | # Package io.islandtime.format 26 | 27 | Formatting of dates, times, durations, and intervals into textual representations. 28 | 29 | # Package io.islandtime.jvm 30 | 31 | Various extensions specific to the Java platform. 32 | 33 | # Package io.islandtime.locale 34 | 35 | Platform-independent locale. 36 | 37 | # Package io.islandtime.measures 38 | 39 | Classes related to the measurement of time, including `Duration`, `Period`, and more specific units, such as `Hours` or `Years`. 40 | 41 | # Package io.islandtime.parser 42 | 43 | Parsing of dates, times, durations, and intervals from textual representations. 44 | 45 | # Package io.islandtime.ranges 46 | 47 | Date ranges, time intervals, and the ability to iterate over them and perform various operations. 48 | 49 | # Package io.islandtime.serialization 50 | 51 | Serializers for use with Kotlin Serialization. 52 | 53 | # Package io.islandtime.zone 54 | 55 | Provides time zone database access. 56 | -------------------------------------------------------------------------------- /core/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `published-mpp-library` 3 | alias(libs.plugins.atomicfu) 4 | alias(libs.plugins.serialization) 5 | alias(libs.plugins.kover) 6 | } 7 | 8 | kotlin { 9 | jvm { 10 | withJava() 11 | } 12 | 13 | sourceSets { 14 | all { 15 | resources.setSrcDirs(emptyList()) 16 | } 17 | 18 | val commonMain by getting { 19 | kotlin.srcDirs("src/commonMain/generated") 20 | 21 | dependencies { 22 | implementation(libs.javamath2kmp) 23 | compileOnly(libs.serializationCore) 24 | } 25 | } 26 | 27 | val commonTest by getting { 28 | dependencies { 29 | implementation(libs.kotlinTest) 30 | } 31 | } 32 | 33 | val jvmMain by getting { 34 | resources.srcDirs("src/jvmMain/resources") 35 | } 36 | 37 | val jvmTest by getting { 38 | dependencies { 39 | implementation(libs.truth) 40 | } 41 | } 42 | 43 | val darwinMain by getting { 44 | dependencies { 45 | api(libs.serializationCore) 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /core/gradle.properties: -------------------------------------------------------------------------------- 1 | pomName=Island Time Core 2 | pomDescription=A multiplatform library for working with dates and times 3 | -------------------------------------------------------------------------------- /core/src/commonMain/generated/io/islandtime/_Conversions.kt: -------------------------------------------------------------------------------- 1 | // 2 | // This file is auto-generated by 'tools:code-generator' 3 | // 4 | @file:JvmMultifileClass 5 | @file:JvmName("DateTimesKt") 6 | 7 | package io.islandtime 8 | 9 | import kotlin.jvm.JvmMultifileClass 10 | import kotlin.jvm.JvmName 11 | 12 | /** 13 | * Returns this year-month with the precision reduced to the year. 14 | */ 15 | public fun YearMonth.toYear(): Year = Year(year) 16 | 17 | /** 18 | * Returns this date with the precision reduced to the year. 19 | */ 20 | public fun Date.toYear(): Year = Year(year) 21 | 22 | /** 23 | * Returns this date with the precision reduced to the month. 24 | */ 25 | public fun Date.toYearMonth(): YearMonth = YearMonth(year, month) 26 | 27 | /** 28 | * Returns this date-time with the precision reduced to the year. 29 | */ 30 | public fun DateTime.toYear(): Year = date.toYear() 31 | 32 | /** 33 | * Returns this date-time with the precision reduced to the month. 34 | */ 35 | public fun DateTime.toYearMonth(): YearMonth = date.toYearMonth() 36 | 37 | /** 38 | * Returns this date-time with the precision reduced to the year. 39 | */ 40 | public fun OffsetDateTime.toYear(): Year = dateTime.toYear() 41 | 42 | /** 43 | * Returns this date-time with the precision reduced to the month. 44 | */ 45 | public fun OffsetDateTime.toYearMonth(): YearMonth = dateTime.toYearMonth() 46 | 47 | /** 48 | * Returns this date-time with the precision reduced to the year. 49 | */ 50 | public fun ZonedDateTime.toYear(): Year = dateTime.toYear() 51 | 52 | /** 53 | * Returns this date-time with the precision reduced to the month. 54 | */ 55 | public fun ZonedDateTime.toYearMonth(): YearMonth = dateTime.toYearMonth() 56 | -------------------------------------------------------------------------------- /core/src/commonMain/generated/io/islandtime/internal/_Constants.kt: -------------------------------------------------------------------------------- 1 | // 2 | // This file is auto-generated by 'tools:code-generator' 3 | // 4 | @file:JvmMultifileClass 5 | @file:JvmName("ConstantsKt") 6 | 7 | package io.islandtime.`internal` 8 | 9 | import kotlin.Int 10 | import kotlin.Long 11 | import kotlin.PublishedApi 12 | import kotlin.jvm.JvmMultifileClass 13 | import kotlin.jvm.JvmName 14 | 15 | @PublishedApi 16 | internal const val NANOSECONDS_PER_MICROSECOND: Int = 1000 17 | 18 | @PublishedApi 19 | internal const val NANOSECONDS_PER_MILLISECOND: Int = 1000000 20 | 21 | @PublishedApi 22 | internal const val NANOSECONDS_PER_SECOND: Int = 1000000000 23 | 24 | @PublishedApi 25 | internal const val NANOSECONDS_PER_MINUTE: Long = 60000000000L 26 | 27 | @PublishedApi 28 | internal const val NANOSECONDS_PER_HOUR: Long = 3600000000000L 29 | 30 | @PublishedApi 31 | internal const val NANOSECONDS_PER_DAY: Long = 86400000000000L 32 | 33 | @PublishedApi 34 | internal const val MICROSECONDS_PER_MILLISECOND: Int = 1000 35 | 36 | @PublishedApi 37 | internal const val MICROSECONDS_PER_SECOND: Int = 1000000 38 | 39 | @PublishedApi 40 | internal const val MICROSECONDS_PER_MINUTE: Int = 60000000 41 | 42 | @PublishedApi 43 | internal const val MICROSECONDS_PER_HOUR: Long = 3600000000L 44 | 45 | @PublishedApi 46 | internal const val MICROSECONDS_PER_DAY: Long = 86400000000L 47 | 48 | @PublishedApi 49 | internal const val MILLISECONDS_PER_SECOND: Int = 1000 50 | 51 | @PublishedApi 52 | internal const val MILLISECONDS_PER_MINUTE: Int = 60000 53 | 54 | @PublishedApi 55 | internal const val MILLISECONDS_PER_HOUR: Int = 3600000 56 | 57 | @PublishedApi 58 | internal const val MILLISECONDS_PER_DAY: Int = 86400000 59 | 60 | @PublishedApi 61 | internal const val SECONDS_PER_MINUTE: Int = 60 62 | 63 | @PublishedApi 64 | internal const val SECONDS_PER_HOUR: Int = 3600 65 | 66 | @PublishedApi 67 | internal const val SECONDS_PER_DAY: Int = 86400 68 | 69 | @PublishedApi 70 | internal const val MINUTES_PER_HOUR: Int = 60 71 | 72 | @PublishedApi 73 | internal const val MINUTES_PER_DAY: Int = 1440 74 | 75 | @PublishedApi 76 | internal const val HOURS_PER_DAY: Int = 24 77 | 78 | @PublishedApi 79 | internal const val DAYS_PER_WEEK: Int = 7 80 | 81 | @PublishedApi 82 | internal const val MONTHS_PER_YEAR: Int = 12 83 | 84 | @PublishedApi 85 | internal const val MONTHS_PER_DECADE: Int = 120 86 | 87 | @PublishedApi 88 | internal const val MONTHS_PER_CENTURY: Int = 1200 89 | 90 | @PublishedApi 91 | internal const val YEARS_PER_DECADE: Int = 10 92 | 93 | @PublishedApi 94 | internal const val YEARS_PER_CENTURY: Int = 100 95 | 96 | @PublishedApi 97 | internal const val DECADES_PER_CENTURY: Int = 10 98 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/io/islandtime/DateTimeException.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime 2 | 3 | open class DateTimeException( 4 | message: String? = null, 5 | cause: Throwable? = null 6 | ) : Exception(message, cause) 7 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/io/islandtime/calendar/WeekSettings.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.calendar 2 | 3 | import io.islandtime.DayOfWeek 4 | import io.islandtime.locale.Locale 5 | 6 | /** 7 | * Week-related calendar settings. 8 | * @property firstDayOfWeek The first day of the week. 9 | * @property minimumDaysInFirstWeek The minimum number of days required in the first week of the year. 10 | */ 11 | data class WeekSettings( 12 | val firstDayOfWeek: DayOfWeek, 13 | val minimumDaysInFirstWeek: Int 14 | ) { 15 | init { 16 | require(minimumDaysInFirstWeek in 1..7) { "minimumDaysInFirstWeek must be in 1..7" } 17 | } 18 | 19 | companion object { 20 | /** 21 | * Returns the definition of a week according to the current system settings. This may differ from the 22 | * definition associated with the default locale on platforms that allow this to be customized, such as iOS and 23 | * macOS. 24 | */ 25 | fun systemDefault(): WeekSettings = systemDefaultWeekSettings() 26 | 27 | /** 28 | * The ISO-8601 calendar system's definition of a week, where the first day of the week is Monday and the first 29 | * week of the year has a minimum of 4 days. 30 | */ 31 | val ISO = WeekSettings(DayOfWeek.MONDAY, minimumDaysInFirstWeek = 4) 32 | 33 | /** 34 | * A definition of a week that starts on Sunday with a minimum of 1 day in the first week of the year. 35 | */ 36 | val SUNDAY_START = WeekSettings(DayOfWeek.SUNDAY, minimumDaysInFirstWeek = 1) 37 | } 38 | } 39 | 40 | /** 41 | * The default [WeekSettings] associated with this locale. 42 | */ 43 | expect val Locale.weekSettings: WeekSettings 44 | 45 | internal expect fun systemDefaultWeekSettings(): WeekSettings 46 | internal expect val Locale.firstDayOfWeek: DayOfWeek 47 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/io/islandtime/clock/Clock.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("FunctionName", "UNUSED_PARAMETER") 2 | 3 | package io.islandtime.clock 4 | 5 | import io.islandtime.Instant 6 | import io.islandtime.PlatformInstant 7 | import io.islandtime.TimeZone 8 | import io.islandtime.measures.Milliseconds 9 | 10 | /** 11 | * An abstraction providing the current time. 12 | * 13 | * For an implementation that uses the system's clock, see [SystemClock]. [FixedClock] is also available for testing 14 | * purposes. 15 | * 16 | * @see SystemClock 17 | * @see FixedClock 18 | */ 19 | interface Clock { 20 | /** 21 | * The time zone of this clock. 22 | */ 23 | val zone: TimeZone 24 | 25 | /** 26 | * Reads the current number of milliseconds that have elapsed since the Unix epoch of `1970-01-01T00:00` in UTC. 27 | */ 28 | fun readMilliseconds(): Milliseconds 29 | 30 | /** 31 | * Reads the current [Instant]. 32 | */ 33 | fun readInstant(): Instant 34 | 35 | /** 36 | * Reads the current [PlatformInstant]. 37 | */ 38 | fun readPlatformInstant(): PlatformInstant 39 | } 40 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/io/islandtime/clock/FixedClock.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.clock 2 | 3 | import io.islandtime.Instant 4 | import io.islandtime.PlatformInstant 5 | import io.islandtime.TimeZone 6 | import io.islandtime.internal.toPlatformInstant 7 | import io.islandtime.measures.* 8 | 9 | /** 10 | * A clock with a fixed time, suitable for testing. 11 | * 12 | * @param instant the initial instant that the clock should be set to 13 | * @param zone the time zone 14 | */ 15 | class FixedClock( 16 | private var instant: Instant, 17 | override val zone: TimeZone 18 | ) : Clock { 19 | 20 | fun setTo(instant: Instant) { 21 | this.instant = instant 22 | } 23 | 24 | operator fun plusAssign(days: Days) { 25 | instant += days 26 | } 27 | 28 | operator fun plusAssign(hours: Hours) { 29 | instant += hours 30 | } 31 | 32 | operator fun plusAssign(minutes: Minutes) { 33 | instant += minutes 34 | } 35 | 36 | operator fun plusAssign(seconds: Seconds) { 37 | instant += seconds 38 | } 39 | 40 | operator fun plusAssign(milliseconds: Milliseconds) { 41 | instant += milliseconds 42 | } 43 | 44 | operator fun plusAssign(microseconds: Microseconds) { 45 | instant += microseconds 46 | } 47 | 48 | operator fun plusAssign(nanoseconds: Nanoseconds) { 49 | instant += nanoseconds 50 | } 51 | 52 | operator fun minusAssign(days: Days) { 53 | instant -= days 54 | } 55 | 56 | operator fun minusAssign(hours: Hours) { 57 | instant -= hours 58 | } 59 | 60 | operator fun minusAssign(minutes: Minutes) { 61 | instant -= minutes 62 | } 63 | 64 | operator fun minusAssign(seconds: Seconds) { 65 | instant -= seconds 66 | } 67 | 68 | operator fun minusAssign(milliseconds: Milliseconds) { 69 | instant -= milliseconds 70 | } 71 | 72 | operator fun minusAssign(microseconds: Microseconds) { 73 | instant -= microseconds 74 | } 75 | 76 | operator fun minusAssign(nanoseconds: Nanoseconds) { 77 | instant -= nanoseconds 78 | } 79 | 80 | override fun readMilliseconds(): Milliseconds { 81 | return instant.millisecondsSinceUnixEpoch 82 | } 83 | 84 | override fun readInstant(): Instant { 85 | return instant 86 | } 87 | 88 | override fun readPlatformInstant(): PlatformInstant { 89 | return instant.toPlatformInstant() 90 | } 91 | 92 | override fun equals(other: Any?): Boolean { 93 | return other is FixedClock && 94 | instant == other.instant && 95 | zone == other.zone 96 | } 97 | 98 | override fun hashCode(): Int { 99 | return 31 * instant.hashCode() + zone.hashCode() 100 | } 101 | 102 | override fun toString(): String = "FixedClock[$instant, $zone]" 103 | } 104 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/io/islandtime/clock/Now.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.clock 2 | 3 | import io.islandtime.* 4 | import io.islandtime.clock.internal.nowImpl 5 | import io.islandtime.internal.deprecatedToError 6 | 7 | /** 8 | * Gets the current [Instant] from the system clock. 9 | */ 10 | fun Instant.Companion.now(): Instant = now(SystemClock.UTC) 11 | 12 | /** 13 | * Gets the current [Instant] from the provided [clock]. 14 | */ 15 | fun Instant.Companion.now(clock: Clock): Instant = clock.readInstant() 16 | 17 | /** 18 | * Gets the current [Year] from the system clock. 19 | */ 20 | fun Year.Companion.now(): Year = now(SystemClock()) 21 | 22 | /** 23 | * Gets the current [Year] from the provided [clock]. 24 | */ 25 | fun Year.Companion.now(clock: Clock): Year = Date.now(clock).toYear() 26 | 27 | /** 28 | * Gets the current [YearMonth] from the system clock. 29 | */ 30 | fun YearMonth.Companion.now(): YearMonth = now(SystemClock()) 31 | 32 | /** 33 | * Gets the current [YearMonth] from the provided [clock]. 34 | */ 35 | fun YearMonth.Companion.now(clock: Clock): YearMonth = Date.now(clock).toYearMonth() 36 | 37 | /** 38 | * Gets the current [Date] from the system clock. 39 | */ 40 | fun Date.Companion.now(): Date = nowImpl(SystemClock()) 41 | 42 | /** 43 | * Gets the current [Date] from the provided [clock]. 44 | */ 45 | fun Date.Companion.now(clock: Clock): Date = nowImpl(clock) 46 | 47 | /** 48 | * Gets the current [DateTime] from the system clock. 49 | */ 50 | fun DateTime.Companion.now(): DateTime = nowImpl(SystemClock()) 51 | 52 | /** 53 | * Gets the current [DateTime] from the provided [clock]. 54 | */ 55 | fun DateTime.Companion.now(clock: Clock): DateTime = nowImpl(clock) 56 | 57 | /** 58 | * Gets the current [OffsetDateTime] from the system clock. 59 | */ 60 | fun OffsetDateTime.Companion.now(): OffsetDateTime = nowImpl(SystemClock()) 61 | 62 | /** 63 | * Gets the current [OffsetDateTime] from the provided [clock]. 64 | */ 65 | fun OffsetDateTime.Companion.now(clock: Clock): OffsetDateTime = nowImpl(clock) 66 | 67 | /** 68 | * Gets the current [ZonedDateTime] from the system clock. 69 | */ 70 | fun ZonedDateTime.Companion.now(): ZonedDateTime = nowImpl(SystemClock()) 71 | 72 | /** 73 | * Gets the current [ZonedDateTime] from the provided [clock]. 74 | */ 75 | fun ZonedDateTime.Companion.now(clock: Clock): ZonedDateTime = nowImpl(clock) 76 | 77 | /** 78 | * Gets the current [Time] from the system clock. 79 | */ 80 | fun Time.Companion.now(): Time = nowImpl(SystemClock()) 81 | 82 | /** 83 | * Gets the current [Time] from the provided [clock]. 84 | */ 85 | fun Time.Companion.now(clock: Clock): Time = nowImpl(clock) 86 | 87 | /** 88 | * Gets the current [OffsetTime] from the system clock. 89 | */ 90 | fun OffsetTime.Companion.now(): OffsetTime = nowImpl(SystemClock()) 91 | 92 | /** 93 | * Gets the current [OffsetTime] from the provided [clock]. 94 | */ 95 | fun OffsetTime.Companion.now(clock: Clock): OffsetTime = nowImpl(clock) 96 | 97 | @Suppress("EXTENSION_SHADOWED_BY_MEMBER") 98 | @Deprecated( 99 | "Moved to TimeZone companion object.", 100 | ReplaceWith("systemDefault()"), 101 | DeprecationLevel.ERROR 102 | ) 103 | fun TimeZone.Companion.systemDefault(): TimeZone = deprecatedToError() 104 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/io/islandtime/clock/SystemClock.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("FunctionName") 2 | 3 | package io.islandtime.clock 4 | 5 | import io.islandtime.TimeZone 6 | import io.islandtime.clock.internal.createSystemClock 7 | import io.islandtime.internal.deprecatedToError 8 | 9 | /** 10 | * A clock that provides the time from the current system. 11 | * 12 | * The time zone is treated as an immutable property of the clock, set when it is created. If you wish to follow 13 | * changes to the system clock's configured time zone, you must create a new [SystemClock] in response to any time zone 14 | * changes. 15 | */ 16 | abstract class SystemClock protected constructor() : Clock { 17 | override fun equals(other: Any?): Boolean { 18 | return other is SystemClock && zone == other.zone 19 | } 20 | 21 | override fun hashCode(): Int = zone.hashCode() + 1 22 | override fun toString(): String = "SystemClock[$zone]" 23 | 24 | companion object { 25 | /** 26 | * A system clock in the UTC time zone. 27 | */ 28 | val UTC: SystemClock = createSystemClock(TimeZone.UTC) 29 | 30 | @Deprecated( 31 | "Use TimeZone.systemDefault() instead.", 32 | ReplaceWith("TimeZone.systemDefault()"), 33 | DeprecationLevel.ERROR 34 | ) 35 | fun currentZone(): TimeZone = deprecatedToError() 36 | } 37 | } 38 | 39 | /** 40 | * Creates a [SystemClock], optionally overriding the system's default time zone with another [zone]. 41 | */ 42 | fun SystemClock(zone: TimeZone = TimeZone.systemDefault()): SystemClock = createSystemClock(zone) 43 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/io/islandtime/clock/internal/NowImpl.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.clock.internal 2 | 3 | import io.islandtime.* 4 | import io.islandtime.clock.Clock 5 | 6 | internal expect fun Date.Companion.nowImpl(clock: Clock): Date 7 | internal expect fun DateTime.Companion.nowImpl(clock: Clock): DateTime 8 | internal expect fun OffsetDateTime.Companion.nowImpl(clock: Clock): OffsetDateTime 9 | internal expect fun ZonedDateTime.Companion.nowImpl(clock: Clock): ZonedDateTime 10 | internal expect fun Time.Companion.nowImpl(clock: Clock): Time 11 | internal expect fun OffsetTime.Companion.nowImpl(clock: Clock): OffsetTime 12 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/io/islandtime/clock/internal/SystemClockImpl.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.clock.internal 2 | 3 | import io.islandtime.TimeZone 4 | import io.islandtime.clock.SystemClock 5 | 6 | internal expect fun createSystemClock(zone: TimeZone): SystemClock 7 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/io/islandtime/format/NumberStyle.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.format 2 | 3 | import io.islandtime.locale.Locale 4 | 5 | /** 6 | * The set of characters that should be used when parsing or formatting numbers. 7 | * 8 | * @property zeroDigit The character that represents zero. 9 | * @property plusSign A list of allowed plus sign characters. The first element will be used when formatting. 10 | * @property minusSign A list of allowed minus sign characters. The first element will be used when formatting. 11 | * @property decimalSeparator A list of allowed decimal separator characters. The first element will be used when 12 | * formatting 13 | */ 14 | data class NumberStyle( 15 | val zeroDigit: Char, 16 | val plusSign: List, 17 | val minusSign: List, 18 | val decimalSeparator: List 19 | ) { 20 | init { 21 | require(plusSign.isNotEmpty()) { "At least one plus sign character is required" } 22 | require(minusSign.isNotEmpty()) { "At least one minus sign character is required" } 23 | require(decimalSeparator.isNotEmpty()) { "At least one decimal separator character is required" } 24 | } 25 | 26 | companion object { 27 | /** 28 | * A locale-agnostic set of characters, matching those allowed in the date-time formats defined in ISO-8601. 29 | * 30 | * - Zero: '0' 31 | * - Plus sign: '+' 32 | * - Minus sign: '-' or '−' 33 | * - Decimal separator: '.' or ',' 34 | */ 35 | val DEFAULT = NumberStyle( 36 | zeroDigit = '0', 37 | plusSign = listOf('+'), 38 | minusSign = listOf('-', '−'), 39 | decimalSeparator = listOf('.', ',') 40 | ) 41 | } 42 | } 43 | 44 | /** 45 | * The [NumberStyle] associated with this locale. 46 | */ 47 | expect val Locale.numberStyle: NumberStyle 48 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/io/islandtime/format/TextStyle.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.format 2 | 3 | // TODO: Make this an expect class typealiased to java.time.TextStyle when Android desugaring is stable 4 | /** 5 | * A style of text. The meaning may vary depending on the context. Standalone styles should be used when displaying text 6 | * by itself since some languages have different names in the context of a date and time. 7 | */ 8 | enum class TextStyle { 9 | FULL, 10 | FULL_STANDALONE, 11 | SHORT, 12 | SHORT_STANDALONE, 13 | NARROW, 14 | NARROW_STANDALONE; 15 | 16 | /** 17 | * Is this a standalone style? 18 | */ 19 | fun isStandalone(): Boolean = (ordinal and 1) == 1 20 | 21 | /** 22 | * Convert to a standalone style, if normal. 23 | */ 24 | fun asStandalone(): TextStyle = entries[ordinal or 1] 25 | 26 | /** 27 | * Convert to a normal style, if standalone. 28 | */ 29 | fun asNormal(): TextStyle = entries[ordinal and 1.inv()] 30 | } 31 | 32 | /** 33 | * A time zone text style. 34 | * 35 | * Standard versions indicate the name for standard time, like "Eastern Standard Time". Daylight versions indicate the 36 | * name of daylight savings time, like "Eastern Daylight Time". Generic is agnostic to daylight savings -- ie. 37 | * "Eastern Time". 38 | */ 39 | enum class TimeZoneTextStyle { 40 | STANDARD, 41 | SHORT_STANDARD, 42 | DAYLIGHT_SAVING, 43 | SHORT_DAYLIGHT_SAVING, 44 | GENERIC, 45 | SHORT_GENERIC; 46 | 47 | /** 48 | * Is this a short style? 49 | */ 50 | fun isShort(): Boolean = this == SHORT_STANDARD || this == SHORT_DAYLIGHT_SAVING || this == SHORT_GENERIC 51 | 52 | /** 53 | * Is this a standard style? 54 | */ 55 | fun isStandard(): Boolean = this == STANDARD || this == SHORT_STANDARD 56 | 57 | /** 58 | * Is this a daylight savings style? 59 | */ 60 | fun isDaylightSaving(): Boolean = this == DAYLIGHT_SAVING || this == SHORT_DAYLIGHT_SAVING 61 | 62 | /** 63 | * Is this a generic style? 64 | */ 65 | fun isGeneric(): Boolean = this == GENERIC || this == SHORT_GENERIC 66 | } 67 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/io/islandtime/format/TimeZoneTextProvider.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.format 2 | 3 | import io.islandtime.IslandTime 4 | import io.islandtime.TimeZone 5 | import io.islandtime.locale.Locale 6 | 7 | /** 8 | * An abstraction that allows localized time zone names to be supplied from different data sources. 9 | */ 10 | interface TimeZoneTextProvider { 11 | /** 12 | * Get the localized time zone text. 13 | * 14 | * @param zone the time zone 15 | * @param style the style of the text 16 | * @param locale the locale 17 | * @return the localized time zone text or `null` if unavailable in the specified style 18 | */ 19 | fun timeZoneTextFor(zone: TimeZone, style: TimeZoneTextStyle, locale: Locale): String? = null 20 | 21 | companion object : TimeZoneTextProvider { 22 | override fun timeZoneTextFor(zone: TimeZone, style: TimeZoneTextStyle, locale: Locale): String? { 23 | return IslandTime.timeZoneTextProvider.timeZoneTextFor(zone, style, locale) 24 | } 25 | } 26 | } 27 | /** 28 | * The default provider of localized time zone text for the current platform. 29 | */ 30 | expect object PlatformTimeZoneTextProvider : TimeZoneTextProvider 31 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/io/islandtime/internal/Constants.kt: -------------------------------------------------------------------------------- 1 | @file:JvmMultifileClass 2 | @file:JvmName("ConstantsKt") 3 | 4 | package io.islandtime.internal 5 | 6 | import kotlin.jvm.JvmMultifileClass 7 | import kotlin.jvm.JvmName 8 | 9 | internal const val DAYS_IN_COMMON_YEAR = 365L 10 | internal const val DAYS_PER_400_YEAR_CYCLE = 146_097 11 | internal const val NUMBER_OF_400_YEAR_CYCLES_FROM_0000_TO_1970 = 5L 12 | internal const val LEAP_YEARS_FROM_1970_TO_2000 = 7L 13 | internal const val YEARS_FROM__1970_TO_2000 = 30L 14 | 15 | internal const val DAYS_FROM_0000_TO_1970 = 16 | (DAYS_PER_400_YEAR_CYCLE * NUMBER_OF_400_YEAR_CYCLES_FROM_0000_TO_1970) - 17 | (YEARS_FROM__1970_TO_2000 * DAYS_IN_COMMON_YEAR + LEAP_YEARS_FROM_1970_TO_2000) 18 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/io/islandtime/internal/CopyIfChanged.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.internal 2 | 3 | import io.islandtime.* 4 | 5 | internal fun Time.copyIfChanged(nanosecond: Int): Time { 6 | return if (nanosecond == this.nanosecond) this else copy(nanosecond = nanosecond) 7 | } 8 | 9 | internal fun Date.copyIfChanged(dayOfMonth: Int): Date { 10 | return if (dayOfMonth == this.dayOfMonth) this else copy(dayOfMonth = dayOfMonth) 11 | } 12 | 13 | internal fun DateTime.copyIfChanged(nanosecond: Int): DateTime { 14 | return if (nanosecond == this.nanosecond) this else copy(nanosecond = nanosecond) 15 | } 16 | 17 | internal fun DateTime.copyIfChanged(time: Time): DateTime { 18 | return if (time === this.time) { 19 | this 20 | } else { 21 | copy(time = time) 22 | } 23 | } 24 | 25 | internal fun DateTime.copyIfChanged(date: Date, time: Time): DateTime { 26 | return if (date === this.date && time === this.time) { 27 | this 28 | } else { 29 | copy(date = date, time = time) 30 | } 31 | } 32 | 33 | internal fun OffsetTime.copyIfChanged(time: Time): OffsetTime { 34 | return if (time === this.time) this else copy(time = time) 35 | } 36 | 37 | internal fun OffsetDateTime.copyIfChanged(dateTime: DateTime): OffsetDateTime { 38 | return if (dateTime === this.dateTime) this else copy(dateTime = dateTime) 39 | } 40 | 41 | internal fun ZonedDateTime.copyIfChanged(dateTime: DateTime): ZonedDateTime { 42 | return if (dateTime === this.dateTime) this else copy(dateTime = dateTime) 43 | } 44 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/io/islandtime/internal/Deprecation.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.internal 2 | 3 | internal fun deprecatedToError(): Nothing = throw NotImplementedError("Deprecation level is ERROR") 4 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/io/islandtime/internal/Extensions.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.internal 2 | 3 | /** 4 | * Append a number to a string, padding it with zeros as necessary to reach a desired length 5 | * @param number The number to pad -- must be positive or zero 6 | * @param length Minimum length of the appended string 7 | */ 8 | internal fun StringBuilder.appendZeroPadded(number: Int, length: Int): StringBuilder { 9 | require(length <= 10) { "length must be <= 10" } 10 | val requiredPadding = length - number.lengthInDigits 11 | 12 | if (requiredPadding > 0) { 13 | append(ZERO_PAD[requiredPadding]) 14 | } 15 | 16 | return append(number) 17 | } 18 | 19 | internal fun Int.toZeroPaddedString(length: Int): String { 20 | return buildString { appendZeroPadded(this@toZeroPaddedString, length) } 21 | } 22 | 23 | private inline val Int.lengthInDigits 24 | get() = when { 25 | this < 10 -> 1 26 | this < 100 -> 2 27 | this < 1_000 -> 3 28 | this < 10_000 -> 4 29 | this < 100_000 -> 5 30 | this < 1_000_000 -> 6 31 | this < 10_000_000 -> 7 32 | this < 100_000_000 -> 8 33 | this < 1_000_000_000 -> 9 34 | else -> 10 35 | } 36 | 37 | private val ZERO_PAD = arrayOf( 38 | "", 39 | "0", 40 | "00", 41 | "000", 42 | "0000", 43 | "00000", 44 | "000000", 45 | "0000000", 46 | "00000000", 47 | "000000000" 48 | ) 49 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/io/islandtime/internal/Instants.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.internal 2 | 3 | import dev.erikchristensen.javamath2kmp.floorDiv 4 | import io.islandtime.* 5 | 6 | internal fun toTimeAt(offset: UtcOffset, secondOfUnixEpoch: Long, nanosecond: Int): Time { 7 | val nanosecondOfDay = ((secondOfUnixEpoch % SECONDS_PER_DAY) * NANOSECONDS_PER_SECOND 8 | + nanosecond + offset.totalSeconds.inNanoseconds.value + NANOSECONDS_PER_DAY) % NANOSECONDS_PER_DAY 9 | return Time.fromNanosecondOfDay(nanosecondOfDay) 10 | } 11 | 12 | internal fun toOffsetTimeAt(offset: UtcOffset, secondOfUnixEpoch: Long, nanosecond: Int): OffsetTime { 13 | return toTimeAt(offset, secondOfUnixEpoch, nanosecond) at offset 14 | } 15 | 16 | internal fun toDateAt(offset: UtcOffset, secondOfUnixEpoch: Long): Date { 17 | val dayOfUnixEpoch = (secondOfUnixEpoch + offset.totalSeconds.value) floorDiv SECONDS_PER_DAY 18 | return Date.fromDayOfUnixEpoch(dayOfUnixEpoch) 19 | } 20 | 21 | internal fun toDateTimeAt(offset: UtcOffset, secondOfUnixEpoch: Long, nanosecond: Int): DateTime { 22 | return DateTime.fromSecondOfUnixEpoch(secondOfUnixEpoch, nanosecond, offset) 23 | } 24 | 25 | internal fun toOffsetDateTimeAt(offset: UtcOffset, secondOfUnixEpoch: Long, nanosecond: Int): OffsetDateTime { 26 | return OffsetDateTime.fromSecondOfUnixEpoch(secondOfUnixEpoch, nanosecond, offset) 27 | } 28 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/io/islandtime/internal/PlatformImpl.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.internal 2 | 3 | import io.islandtime.Instant 4 | import io.islandtime.PlatformInstant 5 | import io.islandtime.TimeZone 6 | 7 | internal expect fun systemDefaultTimeZone(): TimeZone 8 | 9 | internal expect fun PlatformInstant.toIslandInstant(): Instant 10 | internal expect fun Instant.toPlatformInstant(): PlatformInstant 11 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/io/islandtime/internal/WeekNumbers.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("NOTHING_TO_INLINE") 2 | 3 | package io.islandtime.internal 4 | 5 | import dev.erikchristensen.javamath2kmp.floorMod 6 | import io.islandtime.* 7 | import io.islandtime.calendar.WeekSettings 8 | import io.islandtime.measures.Weeks 9 | import io.islandtime.measures.weeks 10 | import io.islandtime.measures.years 11 | 12 | internal inline fun Date.weekOfMonthImpl(settings: WeekSettings): Int = weekNumber(dayOfMonth, settings) 13 | internal inline fun Date.weekOfYearImpl(settings: WeekSettings): Int = weekNumber(dayOfYear, settings) 14 | 15 | internal fun Date.weekBasedYearImpl(settings: WeekSettings): Int { 16 | val dayOfYear = dayOfYear 17 | val offset = startOfWeekOffset(dayOfWeek, dayOfYear, settings) 18 | val week = weekNumber(dayOfYear, offset) 19 | 20 | return if (week == 0) { 21 | year - 1 22 | } else { 23 | val weekOfNextYear = weekNumber( 24 | dayOfMonthOrYear = lengthOfYear.toIntUnchecked() + settings.minimumDaysInFirstWeek, 25 | startOfWeekOffset = offset 26 | ) 27 | 28 | if (week >= weekOfNextYear) year + 1 else year 29 | } 30 | } 31 | 32 | internal fun Date.weekOfWeekBasedYearImpl(settings: WeekSettings): Int { 33 | val dayOfYear = dayOfYear 34 | val offset = startOfWeekOffset(dayOfWeek, dayOfYear, settings) 35 | val week = weekNumber(dayOfYear, offset) 36 | 37 | return if (week == 0) { 38 | (toYear() - 1.years).endDate.weekOfWeekBasedYear(settings) 39 | } else { 40 | val weekOfNextYear = weekNumber( 41 | dayOfMonthOrYear = lengthOfYear.toIntUnchecked() + settings.minimumDaysInFirstWeek, 42 | startOfWeekOffset = offset 43 | ) 44 | 45 | if (week >= weekOfNextYear) week - weekOfNextYear + 1 else week 46 | } 47 | } 48 | 49 | internal fun lengthOfWeekBasedYear(weekBasedYear: Int): Weeks { 50 | return lastWeekOfWeekBasedYear(weekBasedYear).weeks 51 | } 52 | 53 | internal fun lastWeekOfWeekBasedYear(weekBasedYear: Int): Int { 54 | val year = Year(weekBasedYear) 55 | val startOfWeekBasedYear = year.startDate 56 | val dayOfWeek = startOfWeekBasedYear.dayOfWeek 57 | val isLongYear = dayOfWeek == DayOfWeek.THURSDAY || (dayOfWeek == DayOfWeek.WEDNESDAY && year.isLeap) 58 | return if (isLongYear) 53 else 52 59 | } 60 | 61 | private fun Date.weekNumber(dayOfMonthOrYear: Int, settings: WeekSettings): Int { 62 | return weekNumber(dayOfWeek, dayOfMonthOrYear, settings) 63 | } 64 | 65 | private fun weekNumber(dayOfWeek: DayOfWeek, dayOfMonthOrYear: Int, settings: WeekSettings): Int { 66 | val offset = startOfWeekOffset(dayOfWeek, dayOfMonthOrYear, settings) 67 | return weekNumber(dayOfMonthOrYear, offset) 68 | } 69 | 70 | private fun startOfWeekOffset(dayOfWeek: DayOfWeek, dayOfMonthOrYear: Int, settings: WeekSettings): Int { 71 | val adjustedDayOfWeek = dayOfWeek.number(settings) 72 | val startOfWeek = (dayOfMonthOrYear - adjustedDayOfWeek) floorMod 7 73 | 74 | return if (startOfWeek >= settings.minimumDaysInFirstWeek) { 75 | 7 - startOfWeek 76 | } else { 77 | -startOfWeek 78 | } 79 | } 80 | 81 | private fun weekNumber(dayOfMonthOrYear: Int, startOfWeekOffset: Int): Int { 82 | return (7 + startOfWeekOffset + dayOfMonthOrYear - 1) / 7 83 | } 84 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/io/islandtime/locale/Locale.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.locale 2 | 3 | /** 4 | * A locale. 5 | * 6 | * On the JVM, this maps to `java.util.Locale`. On Apple platforms, this maps to `NSLocale`. 7 | */ 8 | expect class Locale 9 | 10 | /** 11 | * Gets the current [Locale]. 12 | * 13 | * On the JVM, the `Category` is not used in order to support older Android versions. 14 | */ 15 | expect fun defaultLocale(): Locale 16 | 17 | /** 18 | * Converts an IETF BCP 47 language tag, such as "en-US" or "de-DE", to a [Locale]. 19 | */ 20 | expect fun String.toLocale(): Locale 21 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/io/islandtime/measures/TimeUnit.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.measures 2 | 3 | /** 4 | * A unit of time measurement. 5 | */ 6 | enum class TimeUnit { 7 | NANOSECONDS, 8 | MICROSECONDS, 9 | MILLISECONDS, 10 | SECONDS, 11 | MINUTES, 12 | HOURS, 13 | DAYS 14 | } 15 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/io/islandtime/measures/internal/Extensions.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.measures.internal 2 | 3 | import io.islandtime.internal.NANOSECONDS_PER_SECOND 4 | import io.islandtime.measures.Nanoseconds 5 | import io.islandtime.measures.Seconds 6 | 7 | internal infix fun Nanoseconds.plusUnchecked(seconds: Seconds): Nanoseconds = 8 | Nanoseconds(value + seconds.value * NANOSECONDS_PER_SECOND) 9 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/io/islandtime/parser/DateTimeParseException.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.parser 2 | 3 | import io.islandtime.DateTimeException 4 | 5 | class DateTimeParseException( 6 | message: String? = null, 7 | val parsedString: String? = null, 8 | val errorIndex: Int = 0, 9 | cause: Throwable? = null 10 | ) : DateTimeException(message, cause) 11 | 12 | internal inline fun throwParserFieldResolutionException(parsedText: String): Nothing { 13 | val objectType = T::class.simpleName ?: "Unknown" 14 | 15 | throw DateTimeParseException( 16 | "The provided parser was unable to supply the fields needed to resolve an object of type '$objectType'", 17 | parsedText 18 | ) 19 | } 20 | 21 | internal inline fun throwParserGroupResolutionException( 22 | expectedCount: Int, 23 | actualCount: Int, 24 | parsedText: String 25 | ): Nothing { 26 | val objectType = T::class.simpleName ?: "Unknown" 27 | 28 | throw DateTimeParseException( 29 | "The provided parser was unable resolve an object of type '$objectType'. Expected $expectedCount groups, got " + 30 | "$actualCount.", 31 | parsedText 32 | ) 33 | } 34 | 35 | internal inline fun List.expectingGroupCount( 36 | expected: Int, 37 | parsedText: String 38 | ): List { 39 | if (size != expected) { 40 | throwParserGroupResolutionException(2, size, parsedText) 41 | } 42 | return this 43 | } 44 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/io/islandtime/parser/DateTimeParseResult.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.parser 2 | 3 | import io.islandtime.base.DateTimeField 4 | 5 | /** 6 | * The result of a parsing operation. 7 | */ 8 | data class DateTimeParseResult( 9 | val fields: MutableMap = hashMapOf(), 10 | var timeZoneId: String? = null 11 | ) { 12 | fun isEmpty() = fields.isEmpty() && timeZoneId == null 13 | fun isNotEmpty() = !isEmpty() 14 | 15 | internal fun deepCopy() = DateTimeParseResult(timeZoneId = timeZoneId).apply { 16 | fields.putAll(this@DateTimeParseResult.fields) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/io/islandtime/parser/DateTimeParser.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.parser 2 | 3 | import io.islandtime.parser.internal.DateTimeParseContext 4 | import io.islandtime.parser.internal.DateTimeParserBuilderImpl 5 | 6 | /** 7 | * A parser that converts text into a collection of date-time fields that are understood throughout Island Time. 8 | */ 9 | abstract class DateTimeParser internal constructor() { 10 | /** 11 | * Parse [text] into a [DateTimeParseResult] containing all parsed fields. 12 | * 13 | * @param text text to parse 14 | * @param settings customize parsing behavior 15 | * @return a result containing all of the parsed fields 16 | * @throws DateTimeParseException if parsing failed 17 | */ 18 | fun parse( 19 | text: CharSequence, 20 | settings: DateTimeParserSettings = DateTimeParserSettings.DEFAULT 21 | ): DateTimeParseResult { 22 | val context = DateTimeParseContext(settings) 23 | val endPosition = parse(context, text, 0) 24 | 25 | if (endPosition < 0) { 26 | val errorPosition = endPosition.inv() 27 | throw DateTimeParseException("Parsing failed at index $errorPosition", text.toString(), errorPosition) 28 | } else if (endPosition < text.length) { 29 | throw DateTimeParseException("Unexpected character at index $endPosition", text.toString(), endPosition) 30 | } 31 | 32 | return context.result 33 | } 34 | 35 | /** 36 | * Is this a literal parser? 37 | */ 38 | internal open val isLiteral: Boolean get() = false 39 | 40 | /** 41 | * Returns `true` if the parser never populates values in the result. 42 | */ 43 | internal open val isConst: Boolean get() = false 44 | 45 | internal abstract fun parse(context: DateTimeParseContext, text: CharSequence, position: Int): Int 46 | } 47 | 48 | /** 49 | * Define a custom [DateTimeParser]. 50 | * @see DateTimeParsers 51 | */ 52 | inline fun dateTimeParser(builder: DateTimeParserBuilder.() -> Unit): DateTimeParser { 53 | return DateTimeParserBuilderImpl().apply(builder).build() 54 | } 55 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/io/islandtime/parser/DateTimeParserSettings.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.parser 2 | 3 | import io.islandtime.format.NumberStyle 4 | import io.islandtime.locale.Locale 5 | import io.islandtime.locale.defaultLocale 6 | 7 | /** 8 | * Settings that control the parsing behavior. 9 | * @property numberStyle Defines the set of characters that should be used when parsing numbers. 10 | * @property locale A function that will be invoked to provide a locale if one is needed during parsing. 11 | */ 12 | data class DateTimeParserSettings( 13 | val numberStyle: NumberStyle = NumberStyle.DEFAULT, 14 | val locale: () -> Locale = { defaultLocale() }, 15 | val isCaseSensitive: Boolean = true 16 | ) { 17 | constructor( 18 | numberStyle: NumberStyle = NumberStyle.DEFAULT, 19 | locale: Locale, 20 | isCaseSensitive: Boolean = true 21 | ) : this(numberStyle, { locale }, isCaseSensitive) 22 | 23 | companion object { 24 | /** 25 | * The default parser settings. 26 | */ 27 | val DEFAULT = DateTimeParserSettings() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/io/islandtime/parser/internal/DateTimeParseContext.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.parser.internal 2 | 3 | import io.islandtime.parser.DateTimeParseResult 4 | import io.islandtime.parser.DateTimeParserSettings 5 | 6 | internal class DateTimeParseContext( 7 | val settings: DateTimeParserSettings 8 | ) { 9 | val locale by lazy(LazyThreadSafetyMode.NONE, settings.locale) 10 | var isCaseSensitive = settings.isCaseSensitive 11 | var result = DateTimeParseResult() 12 | } 13 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/io/islandtime/parser/internal/GroupedDateTimeParserBuilder.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.parser.internal 2 | 3 | import io.islandtime.parser.* 4 | import io.islandtime.parser.GroupedDateTimeParserBuilder 5 | 6 | @PublishedApi 7 | internal class GroupedDateTimeParserBuilderImpl : GroupedDateTimeParserBuilder { 8 | private val parsers = mutableListOf() 9 | 10 | override fun group(builder: DateTimeParserBuilder.() -> Unit) { 11 | parsers += DateTimeParserBuilderImpl().apply(builder).build() 12 | } 13 | 14 | override fun literal(char: Char) { 15 | parsers += CharLiteralParserBuilderImpl(char).build() 16 | } 17 | 18 | override fun literal(string: String) { 19 | parsers += StringLiteralParserBuilderImpl(string).build() 20 | } 21 | 22 | override fun anyOf(vararg builders: GroupedDateTimeParserBuilder.() -> Unit) { 23 | val childParsers = builders.map { GroupedDateTimeParserBuilderImpl().apply(it).build() } 24 | 25 | if (childParsers.isNotEmpty()) { 26 | parsers += GroupedDateTimeParser(childParsers, isAnyOf = true) 27 | } 28 | } 29 | 30 | override fun anyOf(vararg childParsers: GroupedDateTimeParser) { 31 | if (childParsers.isNotEmpty()) { 32 | parsers += GroupedDateTimeParser(childParsers.asList(), isAnyOf = true) 33 | } 34 | } 35 | 36 | fun build(): GroupedDateTimeParser { 37 | return GroupedDateTimeParser(parsers) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/io/islandtime/ranges/Builders.kt: -------------------------------------------------------------------------------- 1 | @file:JvmMultifileClass 2 | @file:JvmName("RangesKt") 3 | 4 | package io.islandtime.ranges 5 | 6 | import io.islandtime.* 7 | import io.islandtime.measures.days 8 | import kotlin.jvm.JvmMultifileClass 9 | import kotlin.jvm.JvmName 10 | 11 | /** 12 | * Combines this [DateRange] with a [TimeZone] to create a [ZonedDateTimeInterval] between the start of the first day 13 | * and the end of the last day in [zone]. 14 | */ 15 | infix fun DateRange.at(zone: TimeZone): ZonedDateTimeInterval { 16 | return when { 17 | isEmpty() -> ZonedDateTimeInterval.EMPTY 18 | isUnbounded() -> ZonedDateTimeInterval.UNBOUNDED 19 | start == endInclusive -> { 20 | val zonedStart = start.startOfDayAt(zone) 21 | val zonedEnd = zonedStart + 1.days 22 | zonedStart until zonedEnd 23 | } 24 | else -> { 25 | val start = if (hasUnboundedStart()) { 26 | DateTime.MIN at zone 27 | } else { 28 | start.startOfDayAt(zone) 29 | } 30 | 31 | val end = if (hasUnboundedEnd()) { 32 | DateTime.MAX at zone 33 | } else { 34 | endInclusive.endOfDayAt(zone) 35 | } 36 | 37 | start..end 38 | } 39 | } 40 | } 41 | 42 | /** 43 | * Combines this [DateTimeInterval] with a [TimeZone] to create a [ZonedDateTimeInterval] where both endpoints are in 44 | * [zone]. 45 | * 46 | * Due to daylight savings time transitions, there a few complexities to be aware of. If the local time of either 47 | * endpoint falls within a gap (meaning it doesn't exist), it will be adjusted forward by the length of the gap. If it 48 | * falls within an overlap (meaning the local time exists twice), the earlier offset will be used. 49 | */ 50 | infix fun DateTimeInterval.at(zone: TimeZone): ZonedDateTimeInterval { 51 | return when { 52 | isEmpty() -> ZonedDateTimeInterval.EMPTY 53 | isUnbounded() -> ZonedDateTimeInterval.UNBOUNDED 54 | else -> { 55 | val start = if (hasUnboundedStart()) { 56 | DateTime.MIN at zone 57 | } else { 58 | start at zone 59 | } 60 | 61 | val end = if (hasUnboundedEnd()) { 62 | DateTime.MAX at zone 63 | } else { 64 | endExclusive at zone 65 | } 66 | 67 | start until end 68 | } 69 | } 70 | } 71 | 72 | /** 73 | * Combines this [InstantInterval] with a [TimeZone] to create an equivalent [ZonedDateTimeInterval] where both 74 | * endpoints are in [zone]. 75 | */ 76 | infix fun InstantInterval.at(zone: TimeZone): ZonedDateTimeInterval { 77 | return when { 78 | isEmpty() -> ZonedDateTimeInterval.EMPTY 79 | isUnbounded() -> ZonedDateTimeInterval.UNBOUNDED 80 | else -> { 81 | val start = if (hasUnboundedStart()) { 82 | DateTime.MIN at zone 83 | } else { 84 | start at zone 85 | } 86 | 87 | val end = if (hasUnboundedEnd()) { 88 | DateTime.MAX at zone 89 | } else { 90 | endExclusive at zone 91 | } 92 | 93 | start until end 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/io/islandtime/ranges/DateIterators.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.ranges 2 | 3 | import io.islandtime.Date 4 | import io.islandtime.measures.Days 5 | import io.islandtime.measures.Months 6 | 7 | internal class DateDayProgressionIterator( 8 | first: Date, 9 | last: Date, 10 | private val step: Days 11 | ) : Iterator { 12 | 13 | private val finalElement = last 14 | private var hasNext = if (step.value > 0) first <= last else first >= last 15 | private var next = if (hasNext) first else finalElement 16 | 17 | override fun hasNext() = hasNext 18 | 19 | override fun next(): Date { 20 | val value = next 21 | 22 | if (value == finalElement) { 23 | if (!hasNext) { 24 | throw NoSuchElementException() 25 | } 26 | 27 | hasNext = false 28 | } else { 29 | next += step 30 | } 31 | 32 | return value 33 | } 34 | } 35 | 36 | internal class DateMonthProgressionIterator( 37 | first: Date, 38 | last: Date, 39 | private val step: Months 40 | ) : Iterator { 41 | 42 | private val finalElement = last 43 | private var hasNext = if (step.value > 0) first <= last else first >= last 44 | private var next = if (hasNext) first else finalElement 45 | 46 | override fun hasNext() = hasNext 47 | 48 | override fun next(): Date { 49 | val value = next 50 | 51 | if (value == finalElement) { 52 | if (!hasNext) { 53 | throw NoSuchElementException() 54 | } 55 | 56 | hasNext = false 57 | } else { 58 | next += step 59 | } 60 | 61 | return value 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/io/islandtime/ranges/Interval.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.ranges 2 | 3 | /** 4 | * A half-open or closed interval. 5 | */ 6 | interface Interval { 7 | /** 8 | * The start of this interval, inclusive. 9 | */ 10 | val start: T 11 | 12 | /** 13 | * The end of this interval, inclusive. 14 | */ 15 | val endInclusive: T 16 | 17 | /** 18 | * The end of this interval, exclusive. 19 | */ 20 | val endExclusive: T 21 | 22 | /** 23 | * Checks if this interval's start is unbounded. In ISO-8601 terminology, this is an "open" start. 24 | */ 25 | fun hasUnboundedStart(): Boolean 26 | 27 | /** 28 | * Checks if this interval's end is unbounded. In ISO-8601 terminology, this is an "open" end. 29 | */ 30 | fun hasUnboundedEnd(): Boolean 31 | 32 | /** 33 | * Checks if this interval's start is bounded, meaning it has a finite value. 34 | */ 35 | fun hasBoundedStart(): Boolean = !hasUnboundedStart() 36 | 37 | /** 38 | * Checks if this interval's end is bounded, meaning it has a finite value. 39 | */ 40 | fun hasBoundedEnd(): Boolean = !hasUnboundedEnd() 41 | 42 | /** 43 | * Checks if both the start and end of this interval are bounded, meaning it has a finite range. 44 | */ 45 | fun isBounded(): Boolean = hasBoundedStart() && hasBoundedEnd() 46 | 47 | /** 48 | * Checks if both the start and end of this interval are unbounded, meaning this is an infinite time period in both 49 | * directions. 50 | */ 51 | fun isUnbounded(): Boolean = hasUnboundedStart() && hasUnboundedEnd() 52 | 53 | /** 54 | * Checks if this interval contains [value]. 55 | */ 56 | operator fun contains(value: T): Boolean 57 | 58 | /** 59 | * Checks if this interval is empty. 60 | */ 61 | fun isEmpty(): Boolean 62 | } 63 | 64 | /** 65 | * Checks if this interval contains [value]. 66 | * 67 | * This will always return `false` if [value] is `null`. 68 | */ 69 | fun Interval.contains(value: T?): Boolean = value != null && contains(value) 70 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/io/islandtime/ranges/TimePointIterators.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.ranges 2 | 3 | import io.islandtime.base.TimePoint 4 | import io.islandtime.measures.Nanoseconds 5 | import io.islandtime.measures.Seconds 6 | 7 | internal class TimePointSecondProgressionIterator>( 8 | first: T, 9 | last: T, 10 | private val step: Seconds 11 | ) : Iterator { 12 | 13 | private val finalElement = last 14 | private var hasNext = if (step.value > 0) last > first else last < first 15 | private var next = if (hasNext) first else finalElement 16 | 17 | override fun hasNext() = hasNext 18 | 19 | override fun next(): T { 20 | val value = next 21 | 22 | if (value.isSameInstantAs(finalElement)) { 23 | if (!hasNext) { 24 | throw NoSuchElementException() 25 | } 26 | 27 | hasNext = false 28 | } else { 29 | next += step 30 | } 31 | 32 | return value 33 | } 34 | } 35 | 36 | internal class TimePointNanosecondProgressionIterator>( 37 | first: T, 38 | last: T, 39 | private val step: Nanoseconds 40 | ) : Iterator { 41 | 42 | private val finalElement = last 43 | private var hasNext = if (step.value > 0) last > first else last < first 44 | private var next = if (hasNext) first else finalElement 45 | 46 | override fun hasNext() = hasNext 47 | 48 | override fun next(): T { 49 | val value = next 50 | 51 | if (value.isSameInstantAs(finalElement)) { 52 | if (!hasNext) { 53 | throw NoSuchElementException() 54 | } 55 | 56 | hasNext = false 57 | } else { 58 | next += step 59 | } 60 | 61 | return value 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/io/islandtime/ranges/internal/Common.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.ranges.internal 2 | 3 | import io.islandtime.DateTime 4 | import io.islandtime.measures.* 5 | import io.islandtime.ranges.Interval 6 | 7 | internal val MAX_INCLUSIVE_END_DATE_TIME: DateTime = DateTime.MAX - 2.nanoseconds 8 | 9 | internal fun secondsBetween( 10 | startSecond: Long, 11 | startNanosecond: Int, 12 | endExclusiveSecond: Long, 13 | endExclusiveNanosecond: Int 14 | ): Seconds { 15 | val secondDiff = endExclusiveSecond - startSecond 16 | val nanoDiff = endExclusiveNanosecond - startNanosecond 17 | 18 | return when { 19 | secondDiff > 0 && nanoDiff < 0 -> secondDiff - 1 20 | secondDiff < 0 && nanoDiff > 0 -> secondDiff + 1 21 | else -> secondDiff 22 | }.seconds 23 | } 24 | 25 | internal fun millisecondsBetween( 26 | startSecond: Long, 27 | startNanosecond: Int, 28 | endExclusiveSecond: Long, 29 | endExclusiveNanosecond: Int 30 | ): Milliseconds { 31 | return (endExclusiveSecond - startSecond).seconds + 32 | (endExclusiveNanosecond - startNanosecond).nanoseconds.inWholeMilliseconds 33 | } 34 | 35 | internal fun microsecondsBetween( 36 | startSecond: Long, 37 | startNanosecond: Int, 38 | endExclusiveSecond: Long, 39 | endExclusiveNanosecond: Int 40 | ): Microseconds { 41 | return (endExclusiveSecond - startSecond).seconds + 42 | (endExclusiveNanosecond - startNanosecond).nanoseconds.inWholeMicroseconds 43 | } 44 | 45 | internal fun nanosecondsBetween( 46 | startSecond: Long, 47 | startNanosecond: Int, 48 | endExclusiveSecond: Long, 49 | endExclusiveNanosecond: Int 50 | ): Nanoseconds { 51 | return (endExclusiveSecond - startSecond).seconds + (endExclusiveNanosecond - startNanosecond).nanoseconds 52 | } 53 | 54 | internal inline fun Interval.buildIsoString( 55 | maxElementSize: Int, 56 | inclusive: Boolean, 57 | appendFunction: StringBuilder.(T) -> StringBuilder 58 | ): String { 59 | return if (isEmpty()) { 60 | "" 61 | } else { 62 | buildString(2 * maxElementSize + 1) { 63 | if (hasBoundedStart()) { 64 | appendFunction(start) 65 | } else { 66 | append("..") 67 | } 68 | 69 | append('/') 70 | 71 | if (hasBoundedEnd()) { 72 | appendFunction(if (inclusive) endInclusive else endExclusive) 73 | } else { 74 | append("..") 75 | } 76 | } 77 | } 78 | } 79 | 80 | internal fun throwUnboundedIntervalException(): Nothing { 81 | throw UnsupportedOperationException( 82 | "An interval cannot be represented as a period or duration unless it is bounded" 83 | ) 84 | } 85 | -------------------------------------------------------------------------------- /core/src/commonTest/kotlin/io/islandtime/IslandTimeTest.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime 2 | 3 | import io.islandtime.format.PlatformDateTimeTextProvider 4 | import io.islandtime.zone.PlatformTimeZoneRulesProvider 5 | import kotlin.test.AfterTest 6 | import kotlin.test.BeforeTest 7 | import kotlin.test.Test 8 | import kotlin.test.assertFailsWith 9 | 10 | class IslandTimeTest { 11 | @BeforeTest 12 | fun setUp() { 13 | IslandTime.reset() 14 | } 15 | 16 | @AfterTest 17 | fun tearDown() { 18 | IslandTime.reset() 19 | } 20 | 21 | @Test 22 | fun `double initialization causes an exception`() { 23 | IslandTime.initialize { 24 | dateTimeTextProvider = PlatformDateTimeTextProvider 25 | } 26 | 27 | assertFailsWith { 28 | IslandTime.initialize { 29 | timeZoneRulesProvider = PlatformTimeZoneRulesProvider 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /core/src/commonTest/kotlin/io/islandtime/calendar/WeekSettingsTest.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.calendar 2 | 3 | import io.islandtime.DayOfWeek 4 | import kotlin.test.Test 5 | import kotlin.test.assertEquals 6 | import kotlin.test.assertFailsWith 7 | 8 | class WeekSettingsTest { 9 | @Test 10 | fun `constructor throws an exception when minimumDaysInFirstWeek is out of range`() { 11 | assertFailsWith { WeekSettings(DayOfWeek.SUNDAY, minimumDaysInFirstWeek = 0) } 12 | assertFailsWith { WeekSettings(DayOfWeek.SUNDAY, minimumDaysInFirstWeek = 8) } 13 | } 14 | 15 | @Test 16 | fun `can be constructed when minimumDaysInFirstWeek is in range`() { 17 | (1..7).forEach { 18 | assertEquals(it, WeekSettings(DayOfWeek.SATURDAY, it).minimumDaysInFirstWeek) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /core/src/commonTest/kotlin/io/islandtime/clock/ClockTest.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.clock 2 | 3 | import io.islandtime.Instant 4 | import io.islandtime.TimeZone 5 | import io.islandtime.measures.milliseconds 6 | import kotlin.test.Test 7 | import kotlin.test.assertEquals 8 | import kotlin.test.assertNotEquals 9 | import kotlin.test.assertTrue 10 | 11 | class ClockTest { 12 | @Test 13 | fun `UTC SystemClock`() { 14 | val clock = SystemClock.UTC 15 | 16 | assertTrue { clock.readMilliseconds() > 0L.milliseconds } 17 | assertTrue { clock.readInstant() > Instant.UNIX_EPOCH } 18 | assertEquals(TimeZone.UTC, clock.zone) 19 | } 20 | 21 | @Test 22 | fun `SystemClock without zone`() { 23 | val clock = SystemClock() 24 | 25 | assertTrue { clock.readMilliseconds() > 0L.milliseconds } 26 | assertTrue { clock.readInstant() > Instant.UNIX_EPOCH } 27 | assertNotEquals("", clock.zone.id) 28 | } 29 | 30 | @Test 31 | fun `SystemClock with zone`() { 32 | val clock = SystemClock(TimeZone("America/Denver")) 33 | 34 | assertTrue { clock.readMilliseconds() > 0L.milliseconds } 35 | assertTrue { clock.readInstant() > Instant.UNIX_EPOCH } 36 | assertEquals(TimeZone("America/Denver"), clock.zone) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /core/src/commonTest/kotlin/io/islandtime/format/NumberStyleTest.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.format 2 | 3 | import io.islandtime.locale.toLocale 4 | import kotlin.test.Test 5 | import kotlin.test.assertEquals 6 | import kotlin.test.assertFailsWith 7 | import kotlin.test.todo 8 | 9 | @Suppress("PrivatePropertyName") 10 | class NumberStyleTest { 11 | private val en_US = "en-US".toLocale() 12 | private val de_DE = "de-DE".toLocale() 13 | private val hi_IN_u_nu_native = "hi-IN-u-nu-native".toLocale() 14 | 15 | @Test 16 | fun `throws an exception when given any empty list`() { 17 | assertFailsWith { 18 | NumberStyle.DEFAULT.copy(plusSign = emptyList()) 19 | } 20 | assertFailsWith { 21 | NumberStyle.DEFAULT.copy(minusSign = emptyList()) 22 | } 23 | assertFailsWith { 24 | NumberStyle.DEFAULT.copy(decimalSeparator = emptyList()) 25 | } 26 | } 27 | 28 | @Test 29 | fun `Locale_numberStyle returns a NumberStyle based on the locale`() { 30 | assertEquals( 31 | NumberStyle( 32 | zeroDigit = '0', 33 | plusSign = listOf('+'), 34 | minusSign = listOf('-'), 35 | decimalSeparator = listOf('.') 36 | ), 37 | en_US.numberStyle 38 | ) 39 | 40 | assertEquals( 41 | NumberStyle( 42 | zeroDigit = '0', 43 | plusSign = listOf('+'), 44 | minusSign = listOf('-'), 45 | decimalSeparator = listOf(',') 46 | ), 47 | de_DE.numberStyle 48 | ) 49 | 50 | // Breaks on some JDKs 51 | todo { 52 | assertEquals( 53 | NumberStyle( 54 | zeroDigit = '०', 55 | plusSign = listOf('+'), 56 | minusSign = listOf('-'), 57 | decimalSeparator = listOf('.') 58 | ), 59 | hi_IN_u_nu_native.numberStyle 60 | ) 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /core/src/commonTest/kotlin/io/islandtime/format/TimeZoneTextProviderTest.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.format 2 | 3 | import io.islandtime.TimeZone 4 | import io.islandtime.locale.toLocale 5 | import io.islandtime.test.AbstractIslandTimeTest 6 | import kotlin.test.Test 7 | import kotlin.test.assertEquals 8 | import kotlin.test.assertFalse 9 | import kotlin.test.assertNull 10 | 11 | @Suppress("PrivatePropertyName") 12 | class TimeZoneTextProviderTest : AbstractIslandTimeTest() { 13 | private val en_US = "en-US".toLocale() 14 | private val de_DE = "de-DE".toLocale() 15 | 16 | @Test 17 | fun `timeZoneTextFor returns null when given a fixed offset time zone`() { 18 | listOf( 19 | TimeZone.FixedOffset("-04:00"), 20 | TimeZone.FixedOffset("+00:00"), 21 | TimeZone.FixedOffset("+14:00") 22 | ).forEach { zone -> 23 | TimeZoneTextStyle.values().forEach { style -> 24 | assertNull(TimeZoneTextProvider.timeZoneTextFor(zone, style, en_US)) 25 | assertNull(TimeZoneTextProvider.timeZoneTextFor(zone, style, de_DE)) 26 | } 27 | } 28 | } 29 | 30 | @Test 31 | fun `timeZoneTextFor returns a localized string when available`() { 32 | val zone = TimeZone("America/New_York") 33 | 34 | assertEquals( 35 | "Eastern Standard Time", 36 | TimeZoneTextProvider.timeZoneTextFor(zone, TimeZoneTextStyle.STANDARD, en_US) 37 | ) 38 | assertEquals( 39 | "EST", 40 | TimeZoneTextProvider.timeZoneTextFor(zone, TimeZoneTextStyle.SHORT_STANDARD, en_US) 41 | ) 42 | assertEquals( 43 | "Eastern Daylight Time", 44 | TimeZoneTextProvider.timeZoneTextFor(zone, TimeZoneTextStyle.DAYLIGHT_SAVING, en_US) 45 | ) 46 | assertEquals( 47 | "EDT", 48 | TimeZoneTextProvider.timeZoneTextFor(zone, TimeZoneTextStyle.SHORT_DAYLIGHT_SAVING, en_US) 49 | ) 50 | } 51 | 52 | @Test 53 | fun `timeZoneTextFor returns null when the zone is invalid`() { 54 | val zone = TimeZone("America/Boston") 55 | assertFalse { zone.isValid } 56 | 57 | TimeZoneTextStyle.values().forEach { style -> 58 | assertNull(TimeZoneTextProvider.timeZoneTextFor(zone, style, en_US)) 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /core/src/commonTest/kotlin/io/islandtime/internal/ExtensionsTest.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.internal 2 | 3 | import kotlin.test.Test 4 | import kotlin.test.assertEquals 5 | 6 | class ExtensionsTest { 7 | @Test 8 | fun `StringBuilder_appendZeroPadded pads Int as expected`() { 9 | assertEquals("9", StringBuilder().appendZeroPadded(9, 1).toString()) 10 | assertEquals("0", StringBuilder().appendZeroPadded(0, 1).toString()) 11 | assertEquals("09", StringBuilder().appendZeroPadded(9, 2).toString()) 12 | assertEquals("10", StringBuilder().appendZeroPadded(10, 2).toString()) 13 | assertEquals("0009", StringBuilder().appendZeroPadded(9, 4).toString()) 14 | assertEquals("0060", StringBuilder().appendZeroPadded(60, 4).toString()) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /core/src/commonTest/kotlin/io/islandtime/measures/DaysTest.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.measures 2 | 3 | import kotlin.test.Test 4 | import kotlin.test.assertEquals 5 | import kotlin.test.assertTrue 6 | import kotlin.time.Duration.Companion.days as kotlinDays 7 | 8 | class DaysTest { 9 | @Test 10 | fun `Days can be compared to other Days`() { 11 | assertTrue { 0.days < 1.days } 12 | assertTrue { 0.days == 0.days } 13 | assertTrue { 5.days > (-1).days } 14 | } 15 | 16 | @Test 17 | fun `toString converts zero days to 'P0D'`() { 18 | assertEquals("P0D", 0.days.toString()) 19 | } 20 | 21 | @Test 22 | fun `toString converts to ISO-8601 period representation`() { 23 | assertEquals("P1D", 1.days.toString()) 24 | assertEquals("-P1D", (-1).days.toString()) 25 | } 26 | 27 | @Test 28 | fun `conversion to Kotlin Duration`() { 29 | assertEquals(0.kotlinDays, 0.days.toKotlinDuration()) 30 | assertEquals(1.kotlinDays, 1.days.toKotlinDuration()) 31 | assertEquals((-1).kotlinDays, (-1L).days.toKotlinDuration()) 32 | assertEquals(Long.MIN_VALUE.kotlinDays, Long.MIN_VALUE.days.toKotlinDuration()) 33 | } 34 | 35 | @Test 36 | fun `conversion from Kotlin Duration`() { 37 | assertEquals(0.days, 0.kotlinDays.toIslandDays()) 38 | assertEquals(1.days, 1.kotlinDays.toIslandDays()) 39 | assertEquals((-1).days, (-1).kotlinDays.toIslandDays()) 40 | assertEquals(Long.MIN_VALUE.days, Long.MIN_VALUE.kotlinDays.toIslandDays()) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /core/src/commonTest/kotlin/io/islandtime/measures/HoursTest.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.measures 2 | 3 | import kotlin.test.Test 4 | import kotlin.test.assertEquals 5 | 6 | class HoursTest { 7 | @Test 8 | fun `toComponents breaks the hours up into days and hours`() { 9 | 36.hours.toComponents { days, hours -> 10 | assertEquals(1.days, days) 11 | assertEquals(12.hours, hours) 12 | } 13 | } 14 | 15 | @Test 16 | fun `toComponentValues breaks the hours up into days and hours`() { 17 | 36.hours.toComponentValues { days, hours -> 18 | assertEquals(1L, days) 19 | assertEquals(12, hours) 20 | } 21 | } 22 | 23 | @Test 24 | fun `toString converts zero hours to 'PT0H'`() { 25 | assertEquals("PT0H", 0.hours.toString()) 26 | } 27 | 28 | @Test 29 | fun `toString converts to ISO-8601 period representation`() { 30 | assertEquals("PT1H", 1.hours.toString()) 31 | assertEquals("-PT1H", (-1).hours.toString()) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /core/src/commonTest/kotlin/io/islandtime/measures/MinutesTest.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.measures 2 | 3 | import kotlin.test.Test 4 | import kotlin.test.assertEquals 5 | 6 | class MinutesTest { 7 | @Test 8 | fun `inSeconds converts to seconds`() { 9 | assertEquals((-300).seconds, (-5).minutes.inSeconds) 10 | assertEquals(60L.seconds, 1L.minutes.inSeconds) 11 | } 12 | 13 | @Test 14 | fun `inMilliseconds converts to Milliseconds`() { 15 | assertEquals((-300_000L).milliseconds, (-5).minutes.inMilliseconds) 16 | assertEquals(60_000L.milliseconds, 1L.minutes.inMilliseconds) 17 | } 18 | 19 | @Test 20 | fun `toComponents breaks the minutes up into days + hours + minutes`() { 21 | (1.days + 1.hours + 1.minutes).toComponents { days, hours, minutes -> 22 | assertEquals(1.days, days) 23 | assertEquals(1.hours, hours) 24 | assertEquals(1.minutes, minutes) 25 | } 26 | } 27 | 28 | @Test 29 | fun `toComponentValues breaks the minutes up into days + hours + minutes`() { 30 | (1.days + 1.hours + 1.minutes).toComponentValues { days, hours, minutes -> 31 | assertEquals(1L, days) 32 | assertEquals(1, hours) 33 | assertEquals(1, minutes) 34 | } 35 | } 36 | 37 | @Test 38 | fun `toComponents breaks the minutes up into hours + minutes`() { 39 | (1.days + 1.hours + 1.minutes).toComponents { hours, minutes -> 40 | assertEquals(25.hours, hours) 41 | assertEquals(1.minutes, minutes) 42 | } 43 | } 44 | 45 | @Test 46 | fun `toComponentValues breaks the minutes up into hours + minutes`() { 47 | (1.days + 1.hours + 1.minutes).toComponentValues { hours, minutes -> 48 | assertEquals(25L, hours) 49 | assertEquals(1, minutes) 50 | } 51 | } 52 | 53 | @Test 54 | fun `Minutes_toString converts zero minutes to 'PT0M'`() { 55 | assertEquals("PT0M", 0.minutes.toString()) 56 | } 57 | 58 | @Test 59 | fun `Minutes_toString converts to ISO-8601 period representation`() { 60 | assertEquals("PT1M", 1.minutes.toString()) 61 | assertEquals("-PT1M", (-1).minutes.toString()) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /core/src/commonTest/kotlin/io/islandtime/measures/MonthsTest.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.measures 2 | 3 | import kotlin.test.Test 4 | import kotlin.test.assertEquals 5 | import kotlin.test.assertTrue 6 | 7 | class MonthsTest { 8 | @Test 9 | fun `Months can be compared to other Months`() { 10 | assertTrue { 0.months < 1.months } 11 | assertTrue { 0.months == 0.months } 12 | assertTrue { 5.months > (-1).months } 13 | } 14 | 15 | @Test 16 | fun `adding years to months produces months`() { 17 | assertEquals(13.months, 1.months + 1.years) 18 | } 19 | 20 | @Test 21 | fun `subtracting years from months produces months`() { 22 | assertEquals(0.months, 12.months - 1.years) 23 | } 24 | 25 | @Test 26 | fun `inWholeYears converts months to an equivalent number of full years`() { 27 | assertEquals(1.years, 13.months.inWholeYears) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /core/src/commonTest/kotlin/io/islandtime/operators/TruncationTest.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("PackageDirectoryMismatch") 2 | 3 | package io.islandtime 4 | 5 | import io.islandtime.measures.TimeUnit.* 6 | import io.islandtime.test.AbstractIslandTimeTest 7 | import kotlin.test.Test 8 | import kotlin.test.assertEquals 9 | 10 | class TruncationTest : AbstractIslandTimeTest() { 11 | @Test 12 | fun `truncatedTo_DAYS returns midnight`() { 13 | assertEquals( 14 | Time.MIDNIGHT, 15 | Time(0, 0, 0, 1).truncatedTo(DAYS) 16 | ) 17 | } 18 | 19 | @Test 20 | fun `truncatedTo_HOURS removes components smaller than hours`() { 21 | assertEquals( 22 | Time(1, 0), 23 | Time(1, 2, 3, 4).truncatedTo(HOURS) 24 | ) 25 | } 26 | 27 | @Test 28 | fun `truncatedTo_MINUTES removes components smaller than minutes`() { 29 | assertEquals( 30 | Time(1, 2), 31 | Time(1, 2, 3, 4).truncatedTo(MINUTES) 32 | ) 33 | } 34 | 35 | @Test 36 | fun `truncatedTo_SECONDS removes components smaller than seconds`() { 37 | assertEquals( 38 | Time(1, 2, 3), 39 | Time(1, 2, 3, 4).truncatedTo(SECONDS) 40 | ) 41 | } 42 | 43 | @Test 44 | fun `truncatedTo_MILLISECONDS removes components smaller than milliseconds`() { 45 | assertEquals( 46 | Time(1, 2, 3, 444_000_000), 47 | Time(1, 2, 3, 444_555_666).truncatedTo(MILLISECONDS) 48 | ) 49 | } 50 | 51 | @Test 52 | fun `truncatedTo_MICROSECONDS removes components smaller than microseconds`() { 53 | assertEquals( 54 | Time(1, 2, 3, 444_555_000), 55 | Time(1, 2, 3, 444_555_666).truncatedTo(MICROSECONDS) 56 | ) 57 | } 58 | 59 | @Test 60 | fun `truncatedTo_NANOSECONDS does nothing`() { 61 | assertEquals( 62 | Time(1, 2, 3, 444_555_666), 63 | Time(1, 2, 3, 444_555_666).truncatedTo(NANOSECONDS) 64 | ) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /core/src/commonTest/kotlin/io/islandtime/parser/AnyOfParserTest.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.parser 2 | 3 | import io.islandtime.base.DateTimeField 4 | import kotlin.test.Test 5 | import kotlin.test.assertEquals 6 | import kotlin.test.assertFailsWith 7 | import kotlin.test.assertTrue 8 | 9 | class AnyOfParserTest { 10 | @Test 11 | fun `requires at least 2 arguments`() { 12 | assertFailsWith { 13 | dateTimeParser { 14 | anyOf({ +' ' }) 15 | } 16 | } 17 | 18 | assertFailsWith { 19 | val childParser = dateTimeParser {} 20 | 21 | dateTimeParser { 22 | anyOf(childParser) 23 | } 24 | } 25 | } 26 | 27 | @Test 28 | fun `succeeds when the first child parser succeeds`() { 29 | val parser = dateTimeParser { 30 | anyOf({ 31 | wholeNumber { 32 | associateWith(DateTimeField.YEAR) 33 | } 34 | }, { 35 | wholeNumber(4) { 36 | associateWith(DateTimeField.MONTH_OF_YEAR) 37 | } 38 | }) 39 | } 40 | 41 | val result = parser.parse("2301") 42 | assertEquals(1, result.fields.size) 43 | assertEquals(2301, result.fields[DateTimeField.YEAR]) 44 | } 45 | 46 | @Test 47 | fun `empty child parsers are allowed`() { 48 | val parser = dateTimeParser { 49 | anyOf({ +' ' }, {}) 50 | } 51 | 52 | val result = parser.parse("") 53 | assertTrue { result.isEmpty() } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /core/src/commonTest/kotlin/io/islandtime/parser/CaseSensitivityTest.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.parser 2 | 3 | import kotlin.test.Test 4 | import kotlin.test.assertEquals 5 | import kotlin.test.assertFailsWith 6 | import kotlin.test.assertTrue 7 | 8 | class CaseSensitivityTest { 9 | @Test 10 | fun `blocks can be made case sensitive regardless of parser setting`() { 11 | val parser = dateTimeParser { 12 | +'T' 13 | caseSensitive { 14 | +'T' 15 | } 16 | +'T' 17 | } 18 | 19 | val exception = assertFailsWith { 20 | parser.parse("ttt", DateTimeParserSettings(isCaseSensitive = false)) 21 | } 22 | assertEquals(1, exception.errorIndex) 23 | 24 | parser.parse("tTt", DateTimeParserSettings(isCaseSensitive = false)) 25 | } 26 | 27 | @Test 28 | fun `blocks can be made case insensitive regardless of parser setting`() { 29 | val parser = dateTimeParser { 30 | +'T' 31 | caseInsensitive { 32 | +'T' 33 | } 34 | +'T' 35 | } 36 | 37 | parser.parse("TTT") 38 | parser.parse("TtT") 39 | 40 | val exception = assertFailsWith { parser.parse("Ttt") } 41 | assertEquals(2, exception.errorIndex) 42 | } 43 | 44 | @Test 45 | fun `nested case sensitivity blocks`() { 46 | val parser = dateTimeParser { 47 | caseInsensitive { 48 | +"T" 49 | caseSensitive { 50 | +"T" 51 | } 52 | } 53 | } 54 | 55 | parser.parse("tT") 56 | parser.parse("TT") 57 | 58 | val exception = assertFailsWith { parser.parse("tt") } 59 | assertEquals(1, exception.errorIndex) 60 | } 61 | 62 | @Test 63 | fun `empty blocks are allowed`() { 64 | val parser = dateTimeParser { 65 | caseSensitive {} 66 | caseInsensitive {} 67 | } 68 | 69 | val result = parser.parse("") 70 | assertTrue { result.isEmpty() } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /core/src/commonTest/kotlin/io/islandtime/parser/DateTimeParserTest.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.parser 2 | 3 | import io.islandtime.base.DateTimeField 4 | import kotlin.test.Test 5 | import kotlin.test.assertEquals 6 | import kotlin.test.assertFailsWith 7 | import kotlin.test.assertTrue 8 | 9 | class DateTimeParserTest { 10 | @Test 11 | fun `parses empty strings when the parser is empty`() { 12 | val result = dateTimeParser {}.parse("") 13 | assertTrue { result.isEmpty() } 14 | } 15 | 16 | @Test 17 | fun `throws an exception when there are unexpected characters after all parsers complete`() { 18 | val parser = dateTimeParser { 19 | wholeNumber(1) { 20 | associateWith(DateTimeField.DAY_OF_WEEK) 21 | } 22 | } 23 | 24 | val exception = assertFailsWith { parser.parse("1 ") } 25 | assertEquals(1, exception.errorIndex) 26 | assertEquals("1 ", exception.parsedString) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /core/src/commonTest/kotlin/io/islandtime/parser/OptionalParserTest.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.parser 2 | 3 | import io.islandtime.base.DateTimeField 4 | import kotlin.test.Test 5 | import kotlin.test.assertEquals 6 | import kotlin.test.assertTrue 7 | 8 | class OptionalParserTest { 9 | @Test 10 | fun `optionally takes the child parser's result when successful`() { 11 | val parser = dateTimeParser { 12 | wholeNumber { 13 | associateWith(DateTimeField.MONTH_OF_YEAR) 14 | } 15 | optional { 16 | +'/' 17 | wholeNumber { 18 | associateWith(DateTimeField.YEAR) 19 | } 20 | } 21 | } 22 | 23 | val result1 = parser.parse("13") 24 | assertEquals(1, result1.fields.size) 25 | assertEquals(13, result1.fields[DateTimeField.MONTH_OF_YEAR]) 26 | 27 | val result2 = parser.parse("13/2012") 28 | assertEquals(2, result2.fields.size) 29 | assertEquals(13, result2.fields[DateTimeField.MONTH_OF_YEAR]) 30 | assertEquals(2012, result2.fields[DateTimeField.YEAR]) 31 | } 32 | 33 | @Test 34 | fun `marked as const when child parser is const`() { 35 | val parser = dateTimeParser { 36 | +' ' 37 | optional { 38 | +' ' 39 | } 40 | +'!' 41 | } 42 | 43 | assertTrue { parser.isConst } 44 | 45 | val result = parser.parse(" !") 46 | assertTrue { result.isEmpty() } 47 | } 48 | 49 | @Test 50 | fun `empty blocks are allowed`() { 51 | val result = dateTimeParser { optional {} }.parse("") 52 | assertTrue { result.isEmpty() } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /core/src/commonTest/kotlin/io/islandtime/parser/internal/ParsersTest.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.parser.internal 2 | 3 | import io.islandtime.format.numberStyle 4 | import io.islandtime.locale.toLocale 5 | import kotlin.test.Test 6 | import kotlin.test.assertEquals 7 | import kotlin.test.todo 8 | 9 | @Suppress("PrivatePropertyName") 10 | class ParsersTest { 11 | private val en_US = "en-US".toLocale() 12 | private val hi_IN_u_nu_native = "hi-IN-u-nu-native".toLocale() 13 | 14 | @Test 15 | fun `Char_toDigit converts a character to a digit according to NumberStyle`() { 16 | assertEquals(0, '0'.toDigit(en_US.numberStyle)) 17 | assertEquals(9, '9'.toDigit(en_US.numberStyle)) 18 | 19 | // Breaks on some JDKs 20 | todo { 21 | assertEquals(0, '०'.toDigit(hi_IN_u_nu_native.numberStyle)) 22 | assertEquals(9, '९'.toDigit(hi_IN_u_nu_native.numberStyle)) 23 | } 24 | } 25 | 26 | @Test 27 | fun `Char_toDigit returns -1 when the character isn't considered a digit`() { 28 | assertEquals(-1, '/'.toDigit(en_US.numberStyle)) 29 | assertEquals(-1, ':'.toDigit(en_US.numberStyle)) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /core/src/commonTest/kotlin/io/islandtime/test/AbstractIslandTimeTest.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.test 2 | 3 | import io.islandtime.IslandTime 4 | import io.islandtime.zone.PlatformTimeZoneRulesProvider 5 | import io.islandtime.zone.TimeZoneRulesProvider 6 | import kotlin.test.AfterTest 7 | import kotlin.test.BeforeTest 8 | 9 | abstract class AbstractIslandTimeTest( 10 | private val timeZoneRulesProvider: TimeZoneRulesProvider = PlatformTimeZoneRulesProvider 11 | ) { 12 | @BeforeTest 13 | fun setUp() { 14 | IslandTime.reset() 15 | IslandTime.initializeWith(timeZoneRulesProvider) 16 | } 17 | 18 | @AfterTest 19 | fun tearDown() { 20 | IslandTime.reset() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /core/src/commonTest/kotlin/io/islandtime/test/TestData.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.test 2 | 3 | import io.islandtime.Date 4 | import io.islandtime.Year 5 | 6 | object TestData { 7 | val isoWeekDates 8 | get() = listOf( 9 | Date(2005, 1, 1) to Triple(2004, 53, 6), 10 | Date(2005, 1, 2) to Triple(2004, 53, 7), 11 | Date(2005, 12, 31) to Triple(2005, 52, 6), 12 | Date(2006, 1, 1) to Triple(2005, 52, 7), 13 | Date(2006, 1, 2) to Triple(2006, 1, 1), 14 | Date(2006, 12, 31) to Triple(2006, 52, 7), 15 | Date(2007, 1, 1) to Triple(2007, 1, 1), 16 | Date(2007, 12, 30) to Triple(2007, 52, 7), 17 | Date(2007, 12, 31) to Triple(2008, 1, 1), 18 | Date(2008, 1, 1) to Triple(2008, 1, 2), 19 | Date(2008, 12, 28) to Triple(2008, 52, 7), 20 | Date(2008, 12, 29) to Triple(2009, 1, 1), 21 | Date(2008, 12, 30) to Triple(2009, 1, 2), 22 | Date(2008, 12, 31) to Triple(2009, 1, 3), 23 | Date(2009, 1, 1) to Triple(2009, 1, 4), 24 | Date(2009, 12, 31) to Triple(2009, 53, 4), 25 | Date(2010, 1, 1) to Triple(2009, 53, 5), 26 | Date(2010, 1, 2) to Triple(2009, 53, 6), 27 | Date(2010, 1, 3) to Triple(2009, 53, 7), 28 | Date(2010, 1, 4) to Triple(2010, 1, 1), 29 | Date.MIN to Triple(Year.MIN_VALUE, 1, 1), 30 | Date.MAX to Triple(Year.MAX_VALUE, 52, 5) 31 | ) 32 | 33 | val sundayStartWeekDates 34 | get() = listOf( 35 | Date(2016, 12, 30) to Triple(2016, 53, 6), 36 | Date(2016, 12, 31) to Triple(2016, 53, 7), 37 | Date(2017, 1, 1) to Triple(2017, 1, 1), 38 | Date(2017, 1, 2) to Triple(2017, 1, 2), 39 | Date(2017, 1, 7) to Triple(2017, 1, 7), 40 | Date(2017, 1, 8) to Triple(2017, 2, 1), 41 | Date(2017, 12, 30) to Triple(2017, 52, 7), 42 | Date(2017, 12, 31) to Triple(2018, 1, 1), 43 | Date(2018, 1, 6) to Triple(2018, 1, 7), 44 | Date(2018, 12, 29) to Triple(2018, 52, 7), 45 | Date(2018, 12, 30) to Triple(2019, 1, 1), 46 | Date(2019, 1, 5) to Triple(2019, 1, 7), 47 | Date(2019, 1, 6) to Triple(2019, 2, 1), 48 | Date(2019, 12, 28) to Triple(2019, 52, 7), 49 | Date(2019, 12, 29) to Triple(2020, 1, 1), 50 | Date(2020, 1, 5) to Triple(2020, 2, 1), 51 | Date.MIN to Triple(Year.MIN_VALUE, 1, 2), 52 | Date.MAX to Triple(Year.MAX_VALUE + 1, 1, 6) 53 | ) 54 | } 55 | -------------------------------------------------------------------------------- /core/src/commonTest/kotlin/io/islandtime/zone/PlatformTimeZoneRulesTest.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.zone 2 | 3 | import kotlin.test.Test 4 | import kotlin.test.assertFalse 5 | import kotlin.test.assertTrue 6 | 7 | class PlatformTimeZoneRulesTest { 8 | @Test 9 | fun `databaseVersion doesn't throw an exception`() { 10 | PlatformTimeZoneRulesProvider.databaseVersion 11 | } 12 | 13 | @Test 14 | fun `availableRegionIds is not empty`() { 15 | assertTrue { PlatformTimeZoneRulesProvider.availableRegionIds.isNotEmpty() } 16 | } 17 | 18 | @Test 19 | fun `each available region ID has time zone rules`() { 20 | for (regionId in PlatformTimeZoneRulesProvider.availableRegionIds) { 21 | assertTrue { PlatformTimeZoneRulesProvider.hasRulesFor(regionId) } 22 | PlatformTimeZoneRulesProvider.rulesFor(regionId) 23 | } 24 | } 25 | 26 | @Test 27 | fun `isFixedOffset returns returns true for Etc entries`() { 28 | assertTrue { PlatformTimeZoneRulesProvider.rulesFor("Etc/GMT+10").hasFixedOffset } 29 | assertTrue { PlatformTimeZoneRulesProvider.rulesFor("Etc/GMT-12").hasFixedOffset } 30 | assertTrue { PlatformTimeZoneRulesProvider.rulesFor("Etc/UTC").hasFixedOffset } 31 | } 32 | 33 | @Test 34 | fun `isFixedOffset returns returns false for regions known to have DST`() { 35 | assertFalse { PlatformTimeZoneRulesProvider.rulesFor("America/New_York").hasFixedOffset } 36 | assertFalse { PlatformTimeZoneRulesProvider.rulesFor("Europe/London").hasFixedOffset } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /core/src/darwinMain/kotlin/io/islandtime/PlatformInstant.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime 2 | 3 | import platform.Foundation.NSDate 4 | 5 | actual typealias PlatformInstant = NSDate 6 | -------------------------------------------------------------------------------- /core/src/darwinMain/kotlin/io/islandtime/calendar/WeekSettings.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(UnsafeNumber::class, ExperimentalForeignApi::class) 2 | 3 | package io.islandtime.calendar 4 | 5 | import io.islandtime.DayOfWeek 6 | import io.islandtime.locale.Locale 7 | import io.islandtime.measures.days 8 | import kotlinx.cinterop.ExperimentalForeignApi 9 | import kotlinx.cinterop.UnsafeNumber 10 | import kotlinx.cinterop.convert 11 | import platform.Foundation.NSCalendar 12 | import platform.Foundation.NSCalendarIdentifierGregorian 13 | 14 | actual val Locale.weekSettings: WeekSettings 15 | get() = NSCalendar(NSCalendarIdentifierGregorian) 16 | .also { it.locale = this } 17 | .run { WeekSettings(firstDayOfWeek, minimumDaysInFirstWeek.convert()) } 18 | 19 | internal actual fun systemDefaultWeekSettings(): WeekSettings { 20 | return with(NSCalendar.currentCalendar) { WeekSettings(firstDayOfWeek, minimumDaysInFirstWeek.convert()) } 21 | } 22 | 23 | internal actual val Locale.firstDayOfWeek: DayOfWeek 24 | get() = NSCalendar(NSCalendarIdentifierGregorian).also { it.locale = this }.firstDayOfWeek 25 | 26 | internal val NSCalendar.firstDayOfWeek: DayOfWeek 27 | get() { 28 | val sundayIndexedWeekNumber = firstWeekday.toInt() 29 | return DayOfWeek.SUNDAY + (sundayIndexedWeekNumber - 1).days 30 | } 31 | -------------------------------------------------------------------------------- /core/src/darwinMain/kotlin/io/islandtime/clock/internal/NowImpl.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.clock.internal 2 | 3 | import io.islandtime.* 4 | import io.islandtime.clock.Clock 5 | import io.islandtime.internal.toOffsetTimeAt 6 | import io.islandtime.internal.toTimeAt 7 | 8 | internal actual fun Date.Companion.nowImpl(clock: Clock): Date { 9 | return with(clock) { readInstant().toDateAt(zone) } 10 | } 11 | 12 | internal actual fun DateTime.Companion.nowImpl(clock: Clock): DateTime { 13 | return with(clock) { readInstant().toDateTimeAt(zone) } 14 | } 15 | 16 | internal actual fun OffsetDateTime.Companion.nowImpl(clock: Clock): OffsetDateTime { 17 | return with(clock) { readInstant().toOffsetDateTimeAt(zone) } 18 | } 19 | 20 | internal actual fun ZonedDateTime.Companion.nowImpl(clock: Clock): ZonedDateTime { 21 | return with(clock) { readInstant() at zone } 22 | } 23 | 24 | internal actual fun Time.Companion.nowImpl(clock: Clock): Time { 25 | return with(clock) { readInstant().toTimeAt(zone) } 26 | } 27 | 28 | internal actual fun OffsetTime.Companion.nowImpl(clock: Clock): OffsetTime { 29 | return with(clock) { readInstant().toOffsetTimeAt(zone) } 30 | } 31 | 32 | private fun Instant.toOffsetDateTimeAt(zone: TimeZone): OffsetDateTime { 33 | return this at zone.rules.offsetAt(this) 34 | } 35 | 36 | private fun Instant.toTimeAt(offset: UtcOffset): Time = toTimeAt(offset, secondOfUnixEpoch, nanosecond) 37 | private fun Instant.toTimeAt(zone: TimeZone): Time = toTimeAt(zone.rules.offsetAt(this)) 38 | 39 | private fun Instant.toOffsetTimeAt(offset: UtcOffset): OffsetTime = 40 | toOffsetTimeAt(offset, secondOfUnixEpoch, nanosecond) 41 | 42 | private fun Instant.toOffsetTimeAt(zone: TimeZone): OffsetTime = toOffsetTimeAt(zone.rules.offsetAt(this)) 43 | -------------------------------------------------------------------------------- /core/src/darwinMain/kotlin/io/islandtime/clock/internal/SystemClockImpl.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.clock.internal 2 | 3 | import io.islandtime.Instant 4 | import io.islandtime.PlatformInstant 5 | import io.islandtime.TimeZone 6 | import io.islandtime.clock.SystemClock 7 | import io.islandtime.measures.* 8 | import kotlinx.cinterop.* 9 | import platform.Foundation.NSDate 10 | import platform.posix.gettimeofday 11 | import platform.posix.timeval 12 | 13 | internal actual fun createSystemClock(zone: TimeZone): SystemClock { 14 | return object : SystemClock() { 15 | override val zone: TimeZone = zone 16 | 17 | override fun readMilliseconds(): Milliseconds { 18 | return readSystemTime { seconds, microseconds -> seconds + microseconds.inWholeMilliseconds } 19 | } 20 | 21 | override fun readInstant(): Instant { 22 | return readSystemTime { seconds, microseconds -> Instant(seconds, microseconds.inNanoseconds) } 23 | } 24 | 25 | override fun readPlatformInstant(): PlatformInstant = NSDate() 26 | } 27 | } 28 | 29 | @OptIn(UnsafeNumber::class, ExperimentalForeignApi::class) 30 | private inline fun readSystemTime(action: (seconds: Seconds, microseconds: Microseconds) -> T): T { 31 | return memScoped { 32 | val posixTime = alloc() 33 | gettimeofday(posixTime.ptr, null) 34 | action(posixTime.tv_sec.convert().seconds, posixTime.tv_usec.microseconds) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /core/src/darwinMain/kotlin/io/islandtime/format/NumberStyle.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.format 2 | 3 | import io.islandtime.locale.Locale 4 | import platform.Foundation.NSNumber 5 | import platform.Foundation.NSNumberFormatter 6 | import platform.Foundation.decimalSeparator 7 | 8 | actual val Locale.numberStyle: NumberStyle 9 | get() { 10 | val formatter = NSNumberFormatter().also { it.locale = this } 11 | 12 | return NumberStyle( 13 | zeroDigit = formatter.stringFromNumber(NSNumber(int = 0))?.singleOrNull() ?: '0', 14 | plusSign = listOf(formatter.plusSign.singleOrElse { '+' }), 15 | minusSign = listOf(formatter.minusSign.singleOrElse { '-' }), 16 | decimalSeparator = listOf(decimalSeparator.singleOrElse { '.' }) 17 | ) 18 | } 19 | 20 | private inline fun String.singleOrElse(default: () -> Char): Char { 21 | return if (length == 1) { 22 | this[0] 23 | } else { 24 | default() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /core/src/darwinMain/kotlin/io/islandtime/format/TimeZoneTextProvider.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.format 2 | 3 | import io.islandtime.TimeZone 4 | import io.islandtime.locale.Locale 5 | import platform.Foundation.NSTimeZone 6 | import platform.Foundation.NSTimeZoneNameStyle 7 | import platform.Foundation.localizedName 8 | import platform.Foundation.timeZoneWithName 9 | 10 | actual object PlatformTimeZoneTextProvider : TimeZoneTextProvider { 11 | override fun timeZoneTextFor(zone: TimeZone, style: TimeZoneTextStyle, locale: Locale): String? { 12 | return if (zone is TimeZone.Region) { 13 | NSTimeZone.timeZoneWithName(zone.id)?.run { 14 | val darwinStyle = when (style) { 15 | TimeZoneTextStyle.STANDARD -> NSTimeZoneNameStyle.NSTimeZoneNameStyleStandard 16 | TimeZoneTextStyle.SHORT_STANDARD -> NSTimeZoneNameStyle.NSTimeZoneNameStyleShortStandard 17 | TimeZoneTextStyle.DAYLIGHT_SAVING -> NSTimeZoneNameStyle.NSTimeZoneNameStyleDaylightSaving 18 | TimeZoneTextStyle.SHORT_DAYLIGHT_SAVING -> NSTimeZoneNameStyle.NSTimeZoneNameStyleShortDaylightSaving 19 | TimeZoneTextStyle.GENERIC -> NSTimeZoneNameStyle.NSTimeZoneNameStyleGeneric 20 | TimeZoneTextStyle.SHORT_GENERIC -> NSTimeZoneNameStyle.NSTimeZoneNameStyleShortGeneric 21 | } 22 | 23 | localizedName(darwinStyle, locale) 24 | } 25 | } else { 26 | null 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /core/src/darwinMain/kotlin/io/islandtime/internal/PlatformImpl.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.internal 2 | 3 | import io.islandtime.Instant 4 | import io.islandtime.PlatformInstant 5 | import io.islandtime.TimeZone 6 | import io.islandtime.darwin.toIslandInstant 7 | import io.islandtime.darwin.toIslandTimeZone 8 | import io.islandtime.darwin.toNSDate 9 | import platform.Foundation.NSTimeZone 10 | import platform.Foundation.localTimeZone 11 | 12 | internal actual fun systemDefaultTimeZone(): TimeZone = NSTimeZone.localTimeZone.toIslandTimeZone() 13 | 14 | internal actual fun PlatformInstant.toIslandInstant(): Instant = toIslandInstant() 15 | internal actual fun Instant.toPlatformInstant(): PlatformInstant = toNSDate() 16 | -------------------------------------------------------------------------------- /core/src/darwinMain/kotlin/io/islandtime/locale/Locale.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.locale 2 | 3 | import platform.Foundation.NSLocale 4 | import platform.Foundation.currentLocale 5 | 6 | actual typealias Locale = NSLocale 7 | 8 | actual fun defaultLocale(): Locale = NSLocale.currentLocale 9 | actual fun String.toLocale(): Locale = NSLocale(this) 10 | -------------------------------------------------------------------------------- /core/src/darwinTest/kotlin/io/islandtime/format/DarwinNumberFormatTest.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.format 2 | 3 | import io.islandtime.locale.toLocale 4 | import kotlin.test.Test 5 | import kotlin.test.assertEquals 6 | 7 | class DarwinNumberFormatTest { 8 | @Test 9 | fun `Locale_numberStyle uses default chars when bidi marks are present`() { 10 | val locale = "ar_EG".toLocale() 11 | 12 | assertEquals( 13 | NumberStyle( 14 | zeroDigit = '٠', 15 | plusSign = listOf('+'), 16 | minusSign = listOf('-'), 17 | decimalSeparator = listOf('٫') 18 | ), 19 | locale.numberStyle 20 | ) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /core/src/darwinTest/kotlin/io/islandtime/format/DarwinTimeZoneTextProviderTest.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.format 2 | 3 | import io.islandtime.TimeZone 4 | import io.islandtime.locale.toLocale 5 | import io.islandtime.test.AbstractIslandTimeTest 6 | import kotlin.test.Test 7 | import kotlin.test.assertEquals 8 | 9 | class DarwinTimeZoneTextProviderTest : AbstractIslandTimeTest() { 10 | @Suppress("PrivatePropertyName") 11 | private val en_US = "en-US".toLocale() 12 | 13 | @Test 14 | fun `timeZoneTextFor returns a localized string for generic styles`() { 15 | val zone = TimeZone("America/New_York") 16 | 17 | assertEquals( 18 | "Eastern Time", 19 | TimeZoneTextProvider.timeZoneTextFor(zone, TimeZoneTextStyle.GENERIC, en_US) 20 | ) 21 | assertEquals( 22 | "ET", 23 | TimeZoneTextProvider.timeZoneTextFor(zone, TimeZoneTextStyle.SHORT_GENERIC, en_US) 24 | ) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /core/src/jvmMain/kotlin/io/islandtime/PlatformInstant.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime 2 | 3 | actual typealias PlatformInstant = java.time.Instant 4 | -------------------------------------------------------------------------------- /core/src/jvmMain/kotlin/io/islandtime/calendar/WeekSettings.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.calendar 2 | 3 | import io.islandtime.DayOfWeek 4 | import io.islandtime.locale.Locale 5 | import io.islandtime.measures.days 6 | import java.util.* 7 | 8 | actual val Locale.weekSettings: WeekSettings 9 | get() { 10 | val gregorianCalendar = GregorianCalendar(this.withoutVariant()) 11 | return with(gregorianCalendar) { WeekSettings(firstIslandDayOfWeek, minimalDaysInFirstWeek) } 12 | } 13 | 14 | internal actual fun systemDefaultWeekSettings(): WeekSettings = Locale.getDefault().weekSettings 15 | 16 | internal actual val Locale.firstDayOfWeek: DayOfWeek 17 | get() = GregorianCalendar(this.withoutVariant()).firstIslandDayOfWeek 18 | 19 | private fun Locale.withoutVariant() = Locale(language, country) 20 | 21 | private val Calendar.firstIslandDayOfWeek: DayOfWeek 22 | get() = DayOfWeek.SUNDAY + (firstDayOfWeek - 1).days 23 | -------------------------------------------------------------------------------- /core/src/jvmMain/kotlin/io/islandtime/clock/internal/Conversions.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.clock.internal 2 | 3 | import io.islandtime.* 4 | import io.islandtime.internal.* 5 | 6 | internal fun java.time.Instant.toTimeAt(offset: UtcOffset): Time = toTimeAt(offset, epochSecond, nano) 7 | internal fun java.time.Instant.toTimeAt(zone: TimeZone): Time = toTimeAt(zone.rules.offsetAt(this)) 8 | 9 | internal fun java.time.Instant.toOffsetTimeAt(offset: UtcOffset): OffsetTime { 10 | return toOffsetTimeAt(offset, epochSecond, nano) 11 | } 12 | 13 | internal fun java.time.Instant.toOffsetTimeAt(zone: TimeZone): OffsetTime { 14 | return toOffsetTimeAt(zone.rules.offsetAt(this)) 15 | } 16 | 17 | internal fun java.time.Instant.toDateAt(offset: UtcOffset): Date = toDateAt(offset, epochSecond) 18 | internal fun java.time.Instant.toDateAt(zone: TimeZone): Date = toDateAt(zone.rules.offsetAt(this)) 19 | 20 | internal fun java.time.Instant.toDateTimeAt(offset: UtcOffset): DateTime = toDateTimeAt(offset, epochSecond, nano) 21 | internal fun java.time.Instant.toDateTimeAt(zone: TimeZone): DateTime = toDateTimeAt(zone.rules.offsetAt(this)) 22 | 23 | internal fun java.time.Instant.toOffsetDateTimeAt(offset: UtcOffset): OffsetDateTime { 24 | return toOffsetDateTimeAt(offset, epochSecond, nano) 25 | } 26 | 27 | internal fun java.time.Instant.toOffsetDateTimeAt(zone: TimeZone): OffsetDateTime { 28 | return toOffsetDateTimeAt(zone.rules.offsetAt(this)) 29 | } 30 | 31 | internal fun java.time.Instant.toZonedDateTimeAt(zone: TimeZone): ZonedDateTime { 32 | val offset = zone.rules.offsetAt(this) 33 | val dateTime = DateTime.fromSecondOfUnixEpoch(epochSecond, nano, offset) 34 | return ZonedDateTime.create(dateTime, offset, zone) 35 | } 36 | -------------------------------------------------------------------------------- /core/src/jvmMain/kotlin/io/islandtime/clock/internal/NowImpl.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.clock.internal 2 | 3 | import io.islandtime.* 4 | import io.islandtime.clock.Clock 5 | 6 | internal actual fun Date.Companion.nowImpl(clock: Clock): Date { 7 | return with(clock) { readPlatformInstant().toDateAt(zone) } 8 | } 9 | 10 | internal actual fun DateTime.Companion.nowImpl(clock: Clock): DateTime { 11 | return with(clock) { readPlatformInstant().toDateTimeAt(zone) } 12 | } 13 | 14 | internal actual fun OffsetDateTime.Companion.nowImpl(clock: Clock): OffsetDateTime { 15 | return with(clock) { readPlatformInstant().toOffsetDateTimeAt(zone) } 16 | } 17 | 18 | internal actual fun ZonedDateTime.Companion.nowImpl(clock: Clock): ZonedDateTime { 19 | return with(clock) { readPlatformInstant().toZonedDateTimeAt(zone) } 20 | } 21 | 22 | internal actual fun Time.Companion.nowImpl(clock: Clock): Time { 23 | return with(clock) { readPlatformInstant().toTimeAt(zone) } 24 | } 25 | 26 | internal actual fun OffsetTime.Companion.nowImpl(clock: Clock): OffsetTime { 27 | return with(clock) { readPlatformInstant().toOffsetTimeAt(zone) } 28 | } 29 | -------------------------------------------------------------------------------- /core/src/jvmMain/kotlin/io/islandtime/clock/internal/SystemClockImpl.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.clock.internal 2 | 3 | import io.islandtime.Instant 4 | import io.islandtime.PlatformInstant 5 | import io.islandtime.TimeZone 6 | import io.islandtime.clock.SystemClock 7 | import io.islandtime.jvm.toIslandInstant 8 | import io.islandtime.measures.Milliseconds 9 | import io.islandtime.measures.milliseconds 10 | import java.time.Clock as JavaClock 11 | 12 | private val javaClock = JavaClock.systemUTC() 13 | 14 | internal actual fun createSystemClock(zone: TimeZone): SystemClock { 15 | return object : SystemClock() { 16 | override val zone: TimeZone = zone 17 | override fun readMilliseconds(): Milliseconds = System.currentTimeMillis().milliseconds 18 | override fun readInstant(): Instant = readPlatformInstant().toIslandInstant() 19 | override fun readPlatformInstant(): PlatformInstant = javaClock.instant() 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /core/src/jvmMain/kotlin/io/islandtime/format/NumberStyle.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.format 2 | 3 | import io.islandtime.locale.Locale 4 | import java.text.DecimalFormatSymbols 5 | 6 | actual val Locale.numberStyle: NumberStyle 7 | get() { 8 | val symbols = DecimalFormatSymbols.getInstance(this) 9 | 10 | return NumberStyle( 11 | zeroDigit = symbols.zeroDigit, 12 | plusSign = listOf('+'), 13 | minusSign = listOf(symbols.minusSign), 14 | decimalSeparator = listOf(symbols.decimalSeparator) 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /core/src/jvmMain/kotlin/io/islandtime/format/TimeZoneTextProvider.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.format 2 | 3 | import io.islandtime.TimeZone 4 | import java.util.* 5 | 6 | actual object PlatformTimeZoneTextProvider : TimeZoneTextProvider { 7 | override fun timeZoneTextFor(zone: TimeZone, style: TimeZoneTextStyle, locale: Locale): String? { 8 | return if (zone is TimeZone.FixedOffset || !zone.isValid || style.isGeneric()) { 9 | null 10 | } else { 11 | val javaTzStyle = if (style.isShort()) java.util.TimeZone.SHORT else java.util.TimeZone.LONG 12 | val isDaylightSaving = style.isDaylightSaving() 13 | 14 | return java.util.TimeZone.getTimeZone(zone.id).getDisplayName(isDaylightSaving, javaTzStyle, locale) 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /core/src/jvmMain/kotlin/io/islandtime/internal/PlatformImpl.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.internal 2 | 3 | import io.islandtime.* 4 | import io.islandtime.jvm.toIslandInstant 5 | import io.islandtime.jvm.toJavaInstant 6 | 7 | internal actual fun systemDefaultTimeZone(): TimeZone = java.util.TimeZone.getDefault().toIslandTimeZone() 8 | 9 | internal actual fun PlatformInstant.toIslandInstant(): Instant = this.toIslandInstant() 10 | internal actual fun Instant.toPlatformInstant(): PlatformInstant = this.toJavaInstant() 11 | 12 | private var lastDefaultTimeZone: Pair? = null 13 | 14 | internal fun java.util.TimeZone.toIslandTimeZone(): TimeZone { 15 | lastDefaultTimeZone?.let { (javaZone, islandZone) -> 16 | if (this == javaZone) { 17 | return islandZone 18 | } 19 | } 20 | 21 | val id = id 22 | val islandZone = if (id.startsWith("GMT")) { 23 | if (id.length == 3) { 24 | TimeZone.Region(id) 25 | } else { 26 | TimeZone.FixedOffset(id.substring(3)) 27 | } 28 | } else { 29 | TimeZone.Region(id) 30 | } 31 | 32 | lastDefaultTimeZone = this to islandZone 33 | return islandZone 34 | } 35 | -------------------------------------------------------------------------------- /core/src/jvmMain/kotlin/io/islandtime/jvm/ClockExtensions.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.jvm 2 | 3 | import io.islandtime.* 4 | import io.islandtime.clock.internal.* 5 | import io.islandtime.internal.toIslandInstant 6 | import java.time.Clock as JavaClock 7 | 8 | /** 9 | * Gets the current [Instant] from the provided [clock]. 10 | */ 11 | fun Instant.Companion.now(clock: JavaClock): Instant = clock.instant().toIslandInstant() 12 | 13 | /** 14 | * Gets the current [Year] from the provided [clock]. 15 | */ 16 | fun Year.Companion.now(clock: JavaClock): Year = Date.now(clock).toYear() 17 | 18 | /** 19 | * Gets the current [YearMonth] from the provided [clock]. 20 | */ 21 | fun YearMonth.Companion.now(clock: JavaClock): YearMonth = Date.now(clock).toYearMonth() 22 | 23 | /** 24 | * Gets the current [Date] from the provided [clock]. 25 | */ 26 | fun Date.Companion.now(clock: JavaClock): Date { 27 | return with(clock) { instant().toDateAt(zone.toIslandTimeZone()) } 28 | } 29 | 30 | /** 31 | * Gets the current [DateTime] from the provided [clock]. 32 | */ 33 | fun DateTime.Companion.now(clock: JavaClock): DateTime { 34 | return with(clock) { instant().toDateTimeAt(zone.toIslandTimeZone()) } 35 | } 36 | 37 | /** 38 | * Gets the current [OffsetDateTime] from the provided [clock]. 39 | */ 40 | fun OffsetDateTime.Companion.now(clock: JavaClock): OffsetDateTime { 41 | return with(clock) { instant().toOffsetDateTimeAt(zone.toIslandTimeZone()) } 42 | } 43 | 44 | /** 45 | * Gets the current [ZonedDateTime] from the provided [clock]. 46 | */ 47 | fun ZonedDateTime.Companion.now(clock: JavaClock): ZonedDateTime { 48 | return with(clock) { instant().toZonedDateTimeAt(zone.toIslandTimeZone()) } 49 | } 50 | 51 | /** 52 | * Gets the current [Time] from the provided [clock]. 53 | */ 54 | fun Time.Companion.now(clock: JavaClock): Time { 55 | return with(clock) { instant().toTimeAt(zone.toIslandTimeZone()) } 56 | } 57 | 58 | /** 59 | * Gets the current [OffsetTime] from the provided [clock]. 60 | */ 61 | fun OffsetTime.Companion.now(clock: JavaClock): OffsetTime { 62 | return with(clock) { instant().toOffsetTimeAt(zone.toIslandTimeZone()) } 63 | } 64 | -------------------------------------------------------------------------------- /core/src/jvmMain/kotlin/io/islandtime/locale/Locale.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.locale 2 | 3 | actual typealias Locale = java.util.Locale 4 | 5 | actual fun defaultLocale(): Locale = Locale.getDefault() 6 | actual fun String.toLocale(): Locale = Locale.forLanguageTag(this) 7 | -------------------------------------------------------------------------------- /core/src/jvmMain/resources/META-INF/proguard/islandtime.pro: -------------------------------------------------------------------------------- 1 | -keepclassmembers class io.islandtime.** { 2 | volatile ; 3 | } 4 | -------------------------------------------------------------------------------- /core/src/jvmTest/java/io/islandtime/jvm/JavaSanityTest.java: -------------------------------------------------------------------------------- 1 | package io.islandtime.jvm; 2 | 3 | import io.islandtime.Date; 4 | import io.islandtime.Month; 5 | import org.junit.Test; 6 | 7 | import java.time.LocalDate; 8 | 9 | import static com.google.common.truth.Truth.assertThat; 10 | import static io.islandtime.jvm.IslandTimeUtils.*; 11 | 12 | public class JavaSanityTest { 13 | @Test 14 | public void convertIslandDateToJavaLocalDate() { 15 | Date islandDate = new Date(2019, Month.MARCH, 1); 16 | LocalDate javaDate = toJavaLocalDate(islandDate); 17 | 18 | assertThat(javaDate.getYear()).isEqualTo(islandDate.getYear()); 19 | assertThat(javaDate.getMonthValue()).isEqualTo(islandDate.getMonth().getNumber()); 20 | assertThat(javaDate.getDayOfMonth()).isEqualTo(islandDate.getDayOfMonth()); 21 | } 22 | 23 | @Test 24 | public void convertJavaLocalDateToIslandDate() { 25 | LocalDate javaDate = LocalDate.of(2019, 3, 1); 26 | Date islandDate = toIslandDate(javaDate); 27 | 28 | assertThat(islandDate.getYear()).isEqualTo(javaDate.getYear()); 29 | assertThat(islandDate.getMonth().getNumber()).isEqualTo(javaDate.getMonthValue()); 30 | assertThat(islandDate.getDayOfMonth()).isEqualTo(javaDate.getDayOfMonth()); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /core/src/jvmTest/kotlin/io/islandtime/calendar/JvmWeekSettingsTest.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.calendar 2 | 3 | import io.islandtime.DayOfWeek 4 | import org.junit.After 5 | import org.junit.Before 6 | import org.junit.Test 7 | import java.util.* 8 | import kotlin.test.assertEquals 9 | 10 | class JvmWeekSettingsTest { 11 | private var previousLocale: Locale? = null 12 | 13 | @Before 14 | fun setUp() { 15 | previousLocale = Locale.getDefault() 16 | } 17 | 18 | @After 19 | fun tearDown() { 20 | Locale.setDefault(previousLocale) 21 | } 22 | 23 | @Test 24 | fun `WeekSettings_systemDefault() in US`() { 25 | Locale.setDefault(Locale.US) 26 | val settings = WeekSettings.systemDefault() 27 | 28 | assertEquals(DayOfWeek.SUNDAY, settings.firstDayOfWeek) 29 | assertEquals(1, settings.minimumDaysInFirstWeek) 30 | } 31 | 32 | @Test 33 | fun `WeekSettings_systemDefault() in Germany`() { 34 | Locale.setDefault(Locale.GERMANY) 35 | val settings = WeekSettings.systemDefault() 36 | 37 | assertEquals(DayOfWeek.MONDAY, settings.firstDayOfWeek) 38 | assertEquals(4, settings.minimumDaysInFirstWeek) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /core/src/jvmTest/kotlin/io/islandtime/format/JvmTimeZoneTextProviderTest.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.format 2 | 3 | import io.islandtime.TimeZone 4 | import io.islandtime.locale.toLocale 5 | import io.islandtime.test.AbstractIslandTimeTest 6 | import org.junit.Test 7 | import kotlin.test.assertNull 8 | 9 | @Suppress("PrivatePropertyName") 10 | class JvmTimeZoneTextProviderTest : AbstractIslandTimeTest() { 11 | private val en_US = "en-US".toLocale() 12 | private val de_DE = "de-DE".toLocale() 13 | 14 | @Test 15 | fun `timeZoneTextFor() returns null for generic styles - limited by Android currently`() { 16 | val zone = TimeZone("America/New_York") 17 | 18 | assertNull(TimeZoneTextProvider.timeZoneTextFor(zone, TimeZoneTextStyle.GENERIC, en_US)) 19 | assertNull(TimeZoneTextProvider.timeZoneTextFor(zone, TimeZoneTextStyle.SHORT_GENERIC, de_DE)) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /core/src/jvmTest/kotlin/io/islandtime/internal/PlatformImplTest.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.internal 2 | 3 | import io.islandtime.DateTimeException 4 | import io.islandtime.TimeZone 5 | import io.islandtime.test.AbstractIslandTimeTest 6 | import org.junit.Test 7 | import kotlin.test.assertEquals 8 | import kotlin.test.assertFailsWith 9 | import kotlin.test.assertTrue 10 | 11 | class PlatformImplTest : AbstractIslandTimeTest() { 12 | @Test 13 | fun `converts java_util_TimeZone with UTC equivalent IDs to Island region-based zones`() { 14 | listOf("GMT", "UTC").forEach { id -> 15 | repeat(2) { 16 | val tz = java.util.TimeZone.getTimeZone(id).toIslandTimeZone() 17 | assertEquals(id, tz.id) 18 | assertTrue { tz.isValid } 19 | } 20 | } 21 | } 22 | 23 | @Test 24 | fun `converts java_util_TimeZone with fixed offset IDs to Island fixed offset zones`() { 25 | val tz1 = java.util.TimeZone.getTimeZone("GMT+18:00").toIslandTimeZone() 26 | assertTrue { tz1 is TimeZone.FixedOffset } 27 | assertEquals("+18:00", tz1.id) 28 | 29 | val tz2 = java.util.TimeZone.getTimeZone("GMT-18:00").toIslandTimeZone() 30 | assertTrue { tz2 is TimeZone.FixedOffset } 31 | assertEquals("-18:00", tz2.id) 32 | } 33 | 34 | @Test 35 | fun `throws exception if converting java_util_TimeZone with fixed offset produces an invalid Island offset`() { 36 | listOf("GMT+18:01", "GMT-18:01").forEach { 37 | assertFailsWith { java.util.TimeZone.getTimeZone(it).toIslandTimeZone() } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /core/src/jvmTest/kotlin/io/islandtime/jvm/DateComparisonTest.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.jvm 2 | 3 | import com.google.common.truth.Truth.assertThat 4 | import io.islandtime.* 5 | import io.islandtime.measures.Period 6 | import org.junit.Test 7 | import java.time.LocalDate 8 | 9 | class DateComparisonTest { 10 | private val javaDates = listOf( 11 | LocalDate.of(2019, java.time.Month.MAY, 3), 12 | LocalDate.of(1970, java.time.Month.JANUARY, 1), 13 | LocalDate.of(1952, java.time.Month.FEBRUARY, 29), 14 | LocalDate.of(1, java.time.Month.JANUARY, 15), 15 | LocalDate.of(0, java.time.Month.JANUARY, 15), 16 | LocalDate.of(-1, java.time.Month.DECEMBER, 15), 17 | LocalDate.MIN, 18 | LocalDate.MAX 19 | ) 20 | 21 | private inline fun List.compareWithIsland(action: (javaDate: LocalDate, islandDate: Date) -> Unit) { 22 | forEach { action(it, it.toIslandDate()) } 23 | } 24 | 25 | @Test 26 | fun `property equivalence`() { 27 | javaDates.compareWithIsland { javaDate, islandDate -> 28 | assertThat(javaDate.dayOfWeek.value).isEqualTo(islandDate.dayOfWeek.number) 29 | assertThat(javaDate.dayOfYear).isEqualTo(islandDate.dayOfYear) 30 | assertThat(javaDate.isLeapYear).isEqualTo(islandDate.isInLeapYear) 31 | assertThat(javaDate.lengthOfMonth()).isEqualTo(islandDate.lengthOfMonth.value) 32 | assertThat(javaDate.lengthOfYear()).isEqualTo(islandDate.lengthOfYear.value) 33 | } 34 | } 35 | 36 | @Test 37 | fun `epoch days`() { 38 | javaDates.compareWithIsland { javaDate, islandDate -> 39 | assertThat(javaDate.toEpochDay()).isEqualTo(islandDate.daysSinceUnixEpoch.value) 40 | } 41 | } 42 | 43 | @Test 44 | fun `period between`() { 45 | val javaEndDate = LocalDate.of(2018, java.time.Month.APRIL, 4) 46 | val islandEndDate = javaEndDate.toIslandDate() 47 | 48 | javaDates.compareWithIsland { javaDate, islandDate -> 49 | val javaPeriod = java.time.Period.between(javaDate, javaEndDate) 50 | val islandPeriod = Period.between(islandDate, islandEndDate) 51 | 52 | assertThat(javaPeriod.days).isEqualTo(islandPeriod.days.value) 53 | assertThat(javaPeriod.months).isEqualTo(islandPeriod.months.value) 54 | assertThat(javaPeriod.years).isEqualTo(islandPeriod.years.value) 55 | assertThat(javaPeriod.negated().days).isEqualTo((-islandPeriod).days.value) 56 | assertThat(javaPeriod.isNegative).isEqualTo(islandPeriod.isNegative()) 57 | assertThat(javaPeriod.normalized().days).isEqualTo(islandPeriod.normalized().days.value) 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /core/src/jvmTest/kotlin/io/islandtime/jvm/DateTimeComparisonTest.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.jvm 2 | 3 | import com.google.common.truth.Truth.assertThat 4 | import io.islandtime.DateTime 5 | import io.islandtime.UtcOffset 6 | import io.islandtime.internal.NANOSECONDS_PER_SECOND 7 | import io.islandtime.measures.milliseconds 8 | import io.islandtime.measures.nanoseconds 9 | import io.islandtime.measures.seconds 10 | import org.junit.Test 11 | import java.time.* 12 | 13 | class DateTimeComparisonTest { 14 | 15 | private fun compare(javaDateTime: LocalDateTime, islandDateTime: DateTime) { 16 | assertThat(javaDateTime.year).isEqualTo(islandDateTime.year) 17 | assertThat(javaDateTime.monthValue).isEqualTo(islandDateTime.monthNumber) 18 | assertThat(javaDateTime.dayOfMonth).isEqualTo(islandDateTime.dayOfMonth) 19 | assertThat(javaDateTime.hour).isEqualTo(islandDateTime.hour) 20 | assertThat(javaDateTime.minute).isEqualTo(islandDateTime.minute) 21 | assertThat(javaDateTime.second).isEqualTo(islandDateTime.second) 22 | assertThat(javaDateTime.nano).isEqualTo(islandDateTime.nanosecond) 23 | assertThat(javaDateTime.toEpochSecond(ZoneOffset.UTC)) 24 | .isEqualTo(islandDateTime.secondsSinceUnixEpochAt(UtcOffset.ZERO).value) 25 | } 26 | 27 | @Test 28 | fun `dateTime from milliseconds since unix epoch`() { 29 | val epochMilliRange = -10_000_000L..10_000_000L 30 | 31 | for (i in epochMilliRange step 100_373) { 32 | val javaDateTime = Instant.ofEpochMilli(i).atOffset(ZoneOffset.UTC).toLocalDateTime() 33 | val islandDateTime = DateTime.fromMillisecondsSinceUnixEpoch(i.milliseconds, UtcOffset.ZERO) 34 | compare(javaDateTime, islandDateTime) 35 | } 36 | } 37 | 38 | @Test 39 | fun `dateTime from seconds since unix epoch`() { 40 | val epochSecondRange = -2L..2L 41 | val nanoRange = 0 until NANOSECONDS_PER_SECOND 42 | 43 | for (second in epochSecondRange) { 44 | for (nano in nanoRange step 100_373) { 45 | val javaDateTime = LocalDateTime.ofEpochSecond(second, nano, ZoneOffset.UTC) 46 | val islandDateTime = 47 | DateTime.fromSecondsSinceUnixEpoch(second.seconds, nano.nanoseconds, UtcOffset.ZERO) 48 | compare(javaDateTime, islandDateTime) 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | islandtime.io -------------------------------------------------------------------------------- /docs/advanced/custom-providers.md: -------------------------------------------------------------------------------- 1 | # Custom Providers 2 | 3 | By default, Island Time uses platform APIs to access time zone database information and localized text. Each platform and version of that platform exposes different information though, so there are compromises involved and Island Time may not always behave the way you'd like. Using custom providers, you can work around certain edge cases or make use of different data sources. 4 | 5 | ## Initialization 6 | 7 | Prior to using Island Time, it may be initialized with custom providers for time zone rules or localized text. The platform default providers will be used for any that aren't specified explicitly. It's only necessary to initialize Island Time if you're using custom providers. 8 | 9 | ```kotlin 10 | IslandTime.initialize { 11 | // Override all of the platform default providers with our own 12 | timeZoneRulesProvider = MyTimeZoneRulesProvider 13 | dateTimeTextProvider = MyDateTimeTextProvider 14 | timeZoneTextProvider = MyTimeZoneTextProvider 15 | } 16 | ``` 17 | 18 | Island Time can only be initialized once. Subsequent attempts to initialize it will throw an exception. This is intended to alert you to potentially undesirable behavior as a result of late initialization or attempts to use different providers in different places. In a test environment though, this can sometimes be problematic, so you may explciitly restore Island Time to an unitialized state using the `reset()` function: 19 | 20 | ```kotlin 21 | IslandTime.reset() 22 | // It's now safe to initialize Island Time again 23 | ``` 24 | -------------------------------------------------------------------------------- /docs/assets/stylesheets/extra.css: -------------------------------------------------------------------------------- 1 | .source-set-tag { 2 | text-align: center; 3 | font-size: x-small; 4 | padding: 4px; 5 | color: black; 6 | background: #FFD54F; 7 | border-radius: 8px 8 | } 9 | 10 | .source-set-common { 11 | color: white; 12 | background: #4DD0E1; 13 | } 14 | 15 | .source-set-jvm { 16 | color: white; 17 | background: #81C784; 18 | } 19 | 20 | .source-set-darwin { 21 | color: white; 22 | background: #9575CD; 23 | } 24 | 25 | .source-set-android { 26 | color: black; 27 | background: #DCE775; 28 | } 29 | -------------------------------------------------------------------------------- /docs/assets/theme/partials/copyright.html: -------------------------------------------------------------------------------- 1 | {#- 2 | This file was automatically generated - do not edit 3 | -#} 4 | 21 | -------------------------------------------------------------------------------- /docs/basics/formatting.md: -------------------------------------------------------------------------------- 1 | # Formatting 2 | 3 | Currently, Island Time lacks the ability to do localized and custom formatting of dates and times in common code. This is in the works and should be available pretty soon. Right now though, it is still possible to access localized text in common code and platform-specific APIs can be used to handle formatting. 4 | 5 | ## Accessing Localized Text 6 | 7 | You can obtain the localized name of a month, day of the week, or time zone in common code like so: 8 | 9 | ```kotlin 10 | // Get the system default locale. We'll assume this is "en_US". 11 | val locale = defaultLocale() 12 | 13 | val shortMonth = FEBRUARY.localizedName(TextStyle.SHORT_STANDALONE, locale) 14 | // Output: "Feb" 15 | 16 | val fullDayOfWeek = TUESDAY.localizedName(TextStyle.FULL_STANDALONE, locale) 17 | // Output: "Tuesday" 18 | 19 | val tz = TimeZone("America/New_York") 20 | val tzName = tz.displayName(TimeZoneTextStyle.DAYLIGHT, locale) 21 | // Output: "Eastern Daylight Time" 22 | ``` 23 | 24 | In general, you'll find a `localizedName()` method that returns `null` if text is unavailable for the provided style and locale. And then a `displayName()` method that will instead return a default value if localized text is unavailable, such as the month or day of week number. 25 | 26 | ## `Locale` 27 | 28 | Island Time's `Locale` is simply a `typealias` for `java.util.Locale` or `NSLocale`. The `defaultLocale()` function allows you to access the user's current locale in common code. A specific locale can be used by converting a [language tag](https://tools.ietf.org/html/bcp47), such as "en-US", using the `String.toLocale()` method. For anything more sophisticated, you should use platform-specific code. 29 | 30 | ## Using Platform APIs 31 | 32 | While you can't share all of your formatting-related code when using platform APIs, there are reasons why you may not necessarily want to do that anyway. 33 | 34 | - Even though Island Time only supports the ISO calendar system, using platform APIs, you can still output to the user's preferred calendar 35 | 36 | - You can better guarantee that formatting will be consistent with the user's expectations for the platform 37 | 38 | - You can take advantage of localization features that may not be available or implemented consistently on all platforms 39 | 40 | ### Java/Android 41 | 42 | ```kotlin 43 | val zone = TimeZone("America/New_York") 44 | val instant = Instant.UNIX_EPOCH 45 | val islandZonedDateTime = instant at zone 46 | 47 | // Create a java.time DateTimeFormatter to do the formatting 48 | val formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.FULL) 49 | 50 | println(islandZonedDateTime.toJavaZonedDateTime().format(formatter)) 51 | // Output: "Wednesday, December 31, 1969 at 7:00:00 PM Eastern Standard Time" 52 | ``` 53 | 54 | ### Apple 55 | 56 | ```kotlin 57 | val zone = TimeZone("America/New_York") 58 | val instant = Instant.UNIX_EPOCH 59 | 60 | // Convert to an NSDate 61 | val nsDate = instant.toNSDate() 62 | 63 | // Create an NSDateFormatter to do the formatting 64 | val formatter = NSDateFormatter().apply { 65 | dateStyle = NSDateFormatterFullStyle 66 | timeStyle = NSDateFormatterFullStyle 67 | timeZone = zone.toNSTimeZone() 68 | } 69 | 70 | println(formatter.stringFromDate(nsDate)) 71 | // Output: "Wednesday, December 31, 1969 at 7:00:00 PM Eastern Standard Time" 72 | ``` 73 | -------------------------------------------------------------------------------- /docs/basics/overview.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | Being heavily inspired by the java.time library, Island Time should look fairly familiar to those acquainted with it and tends to follow many of the same design principles. 4 | 5 | ## General Design 6 | 7 | ### Immutability 8 | 9 | All date-time primitives are immutable and thread-safe. Operations that manipulate a date, time, duration, or interval will always return a new object. 10 | 11 | ### Precision 12 | 13 | Island Time uses integer rather than floating-point values, offering a fixed nanosecond precision across the entire supported time scale. This avoids any surprises that might emerge from the use of floating-point arithmetic and the reduction in precision that occurs when representing larger durations. 14 | 15 | ### Overflow Handling 16 | 17 | When working with dates and times, overflow is almost never a behavior that you want. See [Y2k](https://en.wikipedia.org/wiki/Year_2000_problem) or [Time formatting and storage bugs](https://en.wikipedia.org/wiki/Time_formatting_and_storage_bugs). Island Time uses checked arithmetic throughout to detect overflow and throw exceptions rather than failing silently. 18 | 19 | ### Type-Safety 20 | 21 | In general, Island Time tries to prevent nonsensical operations at compile time rather than runtime. To that end, you'll find that there are a lot more classes than there are in a number of other date-time libraries. 22 | -------------------------------------------------------------------------------- /docs/basics/parsing.md: -------------------------------------------------------------------------------- 1 | # Parsing 2 | 3 | ## Predefined Parsers 4 | 5 | Out of the box, Island Time can parse the most common ISO-8601 formats for dates, times, durations, and time intervals. The set of included parsers can be found in [`DateTimeParsers`](../api/core/core/io.islandtime.parser/-date-time-parsers/index.md). 6 | 7 | The table below illustrates how the parsers for the various ISO formats are organized within `DateTimeParsers`, using the calendar date format as an example: 8 | 9 | | Iso Format | Parser | Acceptable Input(s) | 10 | | --- | --- | --- | 11 | | Basic | `DateTimeParsers.Basic.CALENDAR_DATE` | `20200101` | 12 | | Extended | `DateTimeParsers.Extended.CALENDAR_DATE` | `2020-01-01` | 13 | | Any | `DateTimeParsers.CALENDAR_DATE` | `20200101` or `2020-01-01` | 14 | 15 | The extended format is — by far — the most common. If you don't specify a parser explicitly when converting a string to an Island Time type, it will look for extended format only. Below are some examples: 16 | 17 | ```kotlin 18 | // Parse an extended format date-time 19 | val extendedDateTime = "2020-12-31T13:45".toDateTime() 20 | 21 | // Parse a basic format date-time 22 | val basicDateTime = "20201231T1345".toDateTime(DateTimeParsers.Basic.DATE_TIME) 23 | 24 | // Parse an ordinal date (year and day of year) 25 | val ordinalDate = "2020-365".toDate(DateTimeParsers.Extended.ORDINAL_DATE) 26 | ``` 27 | 28 | ## Custom Parsers 29 | 30 | In an ideal world, non-ISO formats wouldn’t exist, but sometimes they do and you need to parse them. To support that, you can define custom parsers using a DSL. 31 | 32 | ```kotlin 33 | // Define a custom parser 34 | val customParser = dateTimeParser { 35 | monthNumber() 36 | anyOf({ +'/' }, { +'-' }) 37 | dayOfMonth() 38 | optional { 39 | anyOf({ +'/' }, { +'-' }) 40 | year() 41 | } 42 | } 43 | 44 | // Parse a date using it 45 | try { 46 | val date = "3/17/2020".toDate(customParser) 47 | } catch (e: DateTimeException) { 48 | // ... 49 | } 50 | ``` 51 | 52 | When dealing with ranges and intervals, you'll need to define a "grouped" parser, which can handle multiple results. 53 | 54 | ```kotlin 55 | val customGroupedParser = groupedDateTimeParser { 56 | group { 57 | childParser(customParser) 58 | } 59 | +"--" 60 | group { 61 | childParser(customParser) 62 | } 63 | } 64 | 65 | val dateRange = "3/17/2020--4/5/2020".toDateRange(customGroupedParser) 66 | ``` 67 | -------------------------------------------------------------------------------- /docs/basics/serialization.md: -------------------------------------------------------------------------------- 1 | # Serialization 2 | 3 | Island Time includes built-in support for [Kotlin Serialization](https://github.com/Kotlin/kotlinx.serialization). By default, dates, times, durations, and intervals are serialized as ISO-compatible strings. 4 | 5 | ## Serializing to JSON 6 | 7 | For example purposes, let's assume we have a data structure describing an event that we'd like to serialize. 8 | 9 | ```kotlin 10 | @Serializable 11 | data class EventDto( 12 | val name: String, 13 | val dateRange: DateRange, 14 | val createdAt: Instant 15 | ) 16 | ``` 17 | 18 | By using the `@Serializable` annotation, we instruct the Kotlin Serialization plugin to generate a serializer for the `EventDto` class. Island Time's [DateRange](../api/core/core/io.islandtime.ranges/-date-range/index.md) and [Instant](../api/core/core/io.islandtime/-instant/index.md) classes will be automatically serialized as ISO-8601 strings. 19 | 20 | Now, we can serialize the `EventDto` class to JSON with the following code: 21 | 22 | ```kotlin 23 | fun writeToJson(val event: EventDto): String { 24 | val json = Json { prettyPrint = true } 25 | return json.encodeToString(EventDto.serializer(), event) 26 | } 27 | ``` 28 | 29 | Example output might look something like this: 30 | 31 | ```json 32 | { 33 | "name": "KotlinConf 2019", 34 | "dateRange": "2019-12-04/2012-12-06", 35 | "createdAt": "2020-03-14T14:19:03.478Z" 36 | } 37 | ``` 38 | 39 | For more information on how to use Kotlin Serialization, consult the [GitHub page](https://github.com/Kotlin/kotlinx.serialization). 40 | 41 | ## Binary Formats 42 | 43 | At the present time, there are no serializers tuned specifically for binary formats. If you have a use case that requires that, feel free to raise an [issue](https://github.com/erikc5000/island-time/issues). 44 | -------------------------------------------------------------------------------- /docs/extensions/parcelize.md: -------------------------------------------------------------------------------- 1 | #`@Parcelize` 2 | 3 | The `parcelize-extensions` artifact provides a set of parcelers for use with the [Parcelable implementation generator](https://kotlinlang.org/docs/reference/compiler-plugins.html#parcelable-implementations-generator) plugin. 4 | 5 | ## Gradle Setup 6 | 7 | === "Kotlin" 8 | ```kotlin 9 | dependencies { 10 | implementation("io.islandtime:parcelize-extensions:{{ versions.islandtime }}") 11 | } 12 | ``` 13 | 14 | === "Groovy" 15 | ```groovy 16 | dependencies { 17 | implementation "io.islandtime:parcelize-extensions:{{ versions.islandtime }}" 18 | } 19 | ``` 20 | 21 | ## Usage 22 | 23 | Custom parcelers are available for each of Island Time's date-time primitives, durations, and intervals, allowing you to use them within `Parcelable` classes. 24 | 25 | ```kotlin 26 | @Parcelize 27 | @TypeParceler() 28 | data class MyParcelable( 29 | val name: String, 30 | val date: Date 31 | ) : Parcelable 32 | ``` 33 | 34 | In the above example, [DateParceler](../api/extensions/parcelize/parcelize-extensions/io.islandtime.parcelize/-date-parceler/index.md) is used to generate a class containing a non-nullable `Date`. You could make the `Date` nullable instead by using [NullableDateParceler](../api/extensions/parcelize/parcelize-extensions/io.islandtime.parcelize/-nullable-date-parceler/index.md). 35 | 36 | ```kotlin 37 | @Parcelize 38 | @TypeParceler() 39 | data class MyParcelableWithNull( 40 | val name: String, 41 | val date: Date? 42 | ) : Parcelable 43 | ``` 44 | 45 | See the [Parcelize Extensions API documention](../api/extensions/parcelize/index.md) for the full list of available parcelers. 46 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: About 3 | --- 4 | # Island Time 5 | 6 | Island Time is a [Kotlin Multiplatform](https://kotlinlang.org/docs/reference/multiplatform.html) library for working 7 | with dates and times. Heavily inspired by the java.time library, Island Time provides a powerful API that works 8 | across platforms while taking full advantage of the features offered by the Kotlin language. 9 | 10 | ## Features 11 | 12 | - A full set of date-time primitives such as `Date`, `Time`, `DateTime`, `Instant`, and `ZonedDateTime` 13 | - Time zone database support 14 | - Date ranges and time intervals, integrating with Kotlin ranges and progressions 15 | - Read and write strings in ISO formats 16 | - DSL-based definition of custom parsers 17 | - Access localized text for names of months, days of the week, time zones, etc. 18 | - Convenience operators like `date.next(MONDAY)`, `dateTime.startOfWeek`, or `date.week(WeekSettings.systemDefault())` 19 | - Convert to and from platform-specific date-time types 20 | - Works on JVM, Android, iOS, macOS, tvOS, and watchOS 21 | 22 | ## Notable Limitations 23 | - No custom format strings (must write platform-specific code to do this) 24 | - No support for JavaScript or non-Apple native platforms 25 | - Only supports the ISO calendar system 26 | -------------------------------------------------------------------------------- /extensions/parcelize/MODULE.md: -------------------------------------------------------------------------------- 1 | # Module parcelize-extensions 2 | 3 | Parcelers for use with the `@Parcelize` feature in the Kotlin Android Extensions. 4 | -------------------------------------------------------------------------------- /extensions/parcelize/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `published-android-library` 3 | id("kotlin-parcelize") 4 | } 5 | 6 | android { 7 | namespace = "io.islandtime.parcelize" 8 | 9 | compileOptions { 10 | isCoreLibraryDesugaringEnabled = true 11 | } 12 | 13 | defaultConfig { 14 | multiDexEnabled = true 15 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 16 | } 17 | 18 | testOptions { 19 | execution = "ANDROIDX_TEST_ORCHESTRATOR" 20 | } 21 | } 22 | 23 | dependencies { 24 | coreLibraryDesugaring(libs.androidDesugarJdkLibs) 25 | 26 | implementation(project(":core")) 27 | 28 | androidTestImplementation(libs.androidxTestRunner) 29 | androidTestImplementation(libs.truth) 30 | androidTestUtil(libs.androidxTestOrchestrator) 31 | } 32 | -------------------------------------------------------------------------------- /extensions/parcelize/gradle.properties: -------------------------------------------------------------------------------- 1 | pomName=Island Time Parcelize Extensions 2 | pomDescription=Parcelers for use with the Kotlin Android Extensions 3 | pomArtifactId=parcelize-extensions 4 | -------------------------------------------------------------------------------- /extensions/parcelize/src/androidTest/kotlin/DateRangeTest.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.parcelize 2 | 3 | import android.os.Parcelable 4 | import io.islandtime.Date 5 | import io.islandtime.ranges.DateRange 6 | import io.islandtime.Month 7 | import io.islandtime.parcelize.test.testParcelable 8 | import kotlinx.parcelize.Parcelize 9 | import kotlinx.parcelize.TypeParceler 10 | import org.junit.Test 11 | 12 | class DateRangeRangeTest { 13 | @Parcelize 14 | @TypeParceler 15 | data class TestData(val dateRange: DateRange) : Parcelable 16 | 17 | private val testDateRanges = listOf( 18 | DateRange.UNBOUNDED, 19 | DateRange.EMPTY, 20 | Date(2019, Month.NOVEMBER, 3)..Date(2019, Month.DECEMBER, 5) 21 | ) 22 | 23 | @Test 24 | fun dateRangeParceler() { 25 | testDateRanges.forEach { testParcelable(TestData(it)) } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /extensions/parcelize/src/androidTest/kotlin/DateTest.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.parcelize 2 | 3 | import android.os.Parcelable 4 | import io.islandtime.Date 5 | import io.islandtime.Month 6 | import io.islandtime.parcelize.test.testParcelable 7 | import kotlinx.parcelize.Parcelize 8 | import kotlinx.parcelize.TypeParceler 9 | import org.junit.Test 10 | 11 | class DateTest { 12 | @Parcelize 13 | @TypeParceler 14 | data class TestData(val date: Date) : Parcelable 15 | 16 | @Parcelize 17 | @TypeParceler 18 | data class TestNullableData(val date: Date?) : Parcelable 19 | 20 | private val testDates = listOf( 21 | Date.MIN, 22 | Date.MAX, 23 | Date(2019, Month.DECEMBER, 5) 24 | ) 25 | 26 | @Test 27 | fun dateParceler() { 28 | testDates.forEach { testParcelable(TestData(it)) } 29 | } 30 | 31 | @Test 32 | fun nullableDateParceler() { 33 | (testDates + listOf(null)).forEach { testParcelable(TestNullableData(it)) } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /extensions/parcelize/src/androidTest/kotlin/DateTimeIntervalTest.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.parcelize 2 | 3 | import android.os.Parcelable 4 | import io.islandtime.DateTime 5 | import io.islandtime.ranges.DateTimeInterval 6 | import io.islandtime.Month 7 | import io.islandtime.parcelize.test.testParcelable 8 | import io.islandtime.ranges.until 9 | import kotlinx.parcelize.Parcelize 10 | import kotlinx.parcelize.TypeParceler 11 | import org.junit.Test 12 | 13 | class DateTimeIntervalRangeTest { 14 | @Parcelize 15 | @TypeParceler 16 | data class TestData(val dateTimeInterval: DateTimeInterval) : Parcelable 17 | 18 | private val testDateTimeIntervals = listOf( 19 | DateTimeInterval.UNBOUNDED, 20 | DateTimeInterval.EMPTY, 21 | DateTime(2019, Month.NOVEMBER, 3, 1, 2, 3, 4) until 22 | DateTime(2019, Month.DECEMBER, 5, 5, 6, 7, 8) 23 | ) 24 | 25 | @Test 26 | fun dateTimeIntervalParceler() { 27 | testDateTimeIntervals.forEach { testParcelable(TestData(it)) } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /extensions/parcelize/src/androidTest/kotlin/DateTimeTest.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.parcelize 2 | 3 | import android.os.Parcelable 4 | import io.islandtime.DateTime 5 | import io.islandtime.Month 6 | import io.islandtime.parcelize.test.testParcelable 7 | import kotlinx.parcelize.Parcelize 8 | import kotlinx.parcelize.TypeParceler 9 | import org.junit.Test 10 | 11 | class DateTimeTimeTest { 12 | @Parcelize 13 | @TypeParceler 14 | data class TestData(val dateTime: DateTime) : Parcelable 15 | 16 | @Parcelize 17 | @TypeParceler 18 | data class TestNullableData(val dateTime: DateTime?) : Parcelable 19 | 20 | private val testDateTimes = listOf( 21 | DateTime.MIN, 22 | DateTime.MAX, 23 | DateTime(2019, Month.DECEMBER, 5, 1, 2, 3, 4) 24 | ) 25 | 26 | @Test 27 | fun dateTimeParceler() { 28 | testDateTimes.forEach { testParcelable(TestData(it)) } 29 | } 30 | 31 | @Test 32 | fun nullableDateTimeParceler() { 33 | (testDateTimes + listOf(null)).forEach { testParcelable(TestNullableData(it)) } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /extensions/parcelize/src/androidTest/kotlin/DurationTest.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.parcelize 2 | 3 | import android.os.Parcelable 4 | import io.islandtime.parcelize.test.testParcelable 5 | import io.islandtime.measures.Duration 6 | import io.islandtime.measures.durationOf 7 | import io.islandtime.measures.hours 8 | import kotlinx.parcelize.Parcelize 9 | import kotlinx.parcelize.TypeParceler 10 | import org.junit.Test 11 | 12 | class DurationTest { 13 | @Parcelize 14 | @TypeParceler 15 | data class TestData(val duration: Duration) : Parcelable 16 | 17 | @Parcelize 18 | @TypeParceler 19 | data class TestNullableData(val duration: Duration?) : Parcelable 20 | 21 | private val testDurations = listOf( 22 | Duration.MIN, 23 | Duration.MAX, 24 | Duration.ZERO, 25 | durationOf(20.hours) 26 | ) 27 | 28 | @Test 29 | fun durationParceler() { 30 | testDurations.forEach { testParcelable(TestData(it)) } 31 | } 32 | 33 | @Test 34 | fun nullableDurationParceler() { 35 | (testDurations + listOf(null)).forEach { testParcelable(TestNullableData(it)) } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /extensions/parcelize/src/androidTest/kotlin/InstantIntervalTest.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.parcelize 2 | 3 | import android.os.Parcelable 4 | import io.islandtime.Instant 5 | import io.islandtime.ranges.InstantInterval 6 | import io.islandtime.parcelize.test.testParcelable 7 | import io.islandtime.measures.seconds 8 | import io.islandtime.ranges.until 9 | import kotlinx.parcelize.Parcelize 10 | import kotlinx.parcelize.TypeParceler 11 | import org.junit.Test 12 | 13 | class InstantIntervalRangeTest { 14 | @Parcelize 15 | @TypeParceler 16 | data class TestData(val instantInterval: InstantInterval) : Parcelable 17 | 18 | private val testInstantIntervals = listOf( 19 | InstantInterval.UNBOUNDED, 20 | InstantInterval.EMPTY, 21 | Instant((-1L).seconds) until Instant(1L.seconds) 22 | ) 23 | 24 | @Test 25 | fun instantIntervalParceler() { 26 | testInstantIntervals.forEach { testParcelable(TestData(it)) } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /extensions/parcelize/src/androidTest/kotlin/InstantTest.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.parcelize 2 | 3 | import android.os.Parcelable 4 | import io.islandtime.Instant 5 | import io.islandtime.parcelize.test.testParcelable 6 | import kotlinx.parcelize.Parcelize 7 | import kotlinx.parcelize.TypeParceler 8 | import org.junit.Test 9 | 10 | class InstantTest { 11 | @Parcelize 12 | @TypeParceler 13 | data class TestData(val instant: Instant) : Parcelable 14 | 15 | @Parcelize 16 | @TypeParceler 17 | data class TestNullableData(val instant: Instant?) : Parcelable 18 | 19 | private val testInstants = listOf( 20 | Instant.MIN, 21 | Instant.MAX, 22 | Instant.UNIX_EPOCH 23 | ) 24 | 25 | @Test 26 | fun instantParceler() { 27 | testInstants.forEach { testParcelable(TestData(it)) } 28 | } 29 | 30 | @Test 31 | fun nullableInstantParceler() { 32 | (testInstants + listOf(null)).forEach { testParcelable(TestNullableData(it)) } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /extensions/parcelize/src/androidTest/kotlin/OffsetDateTimeIntervalTest.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.parcelize 2 | 3 | import android.os.Parcelable 4 | import io.islandtime.OffsetDateTime 5 | import io.islandtime.ranges.OffsetDateTimeInterval 6 | import io.islandtime.UtcOffset 7 | import io.islandtime.parcelize.test.testParcelable 8 | import io.islandtime.measures.hours 9 | import io.islandtime.ranges.until 10 | import kotlinx.parcelize.Parcelize 11 | import kotlinx.parcelize.TypeParceler 12 | import org.junit.Test 13 | 14 | class OffsetDateTimeIntervalRangeTest { 15 | @Parcelize 16 | @TypeParceler 17 | data class TestData(val offsetDateTimeInterval: OffsetDateTimeInterval) : Parcelable 18 | 19 | private val testOffset = UtcOffset((-4).hours) 20 | 21 | private val testOffsetDateTimeIntervals = listOf( 22 | OffsetDateTimeInterval.UNBOUNDED, 23 | OffsetDateTimeInterval.EMPTY, 24 | OffsetDateTime(2019, 3, 3, 1, 2, 3, testOffset) until 25 | OffsetDateTime(2019, 15, 5, 5, 6, 7, testOffset) 26 | ) 27 | 28 | @Test 29 | fun offsetDateTimeIntervalParceler() { 30 | testOffsetDateTimeIntervals.forEach { testParcelable(TestData(it)) } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /extensions/parcelize/src/androidTest/kotlin/OffsetDateTimeTest.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.parcelize 2 | 3 | import android.os.Parcelable 4 | import io.islandtime.* 5 | import io.islandtime.parcelize.test.testParcelable 6 | import io.islandtime.measures.hours 7 | import kotlinx.parcelize.Parcelize 8 | import kotlinx.parcelize.TypeParceler 9 | import org.junit.Test 10 | 11 | class OffsetDateTimeTimeTest { 12 | @Parcelize 13 | @TypeParceler 14 | data class TestData(val offsetDateTime: OffsetDateTime) : Parcelable 15 | 16 | @Parcelize 17 | @TypeParceler 18 | data class TestNullableData(val offsetDateTime: OffsetDateTime?) : Parcelable 19 | 20 | private val testOffsetDateTimes = listOf( 21 | OffsetDateTime.MIN, 22 | OffsetDateTime.MAX, 23 | DateTime(2019, Month.DECEMBER, 5, 1, 2, 3, 4) 24 | at UtcOffset((-4).hours), 25 | DateTime(1919, Month.DECEMBER, 5, 1, 2, 3, 4) 26 | at UtcOffset.ZERO 27 | ) 28 | 29 | @Test 30 | fun offsetDateTimeParceler() { 31 | testOffsetDateTimes.forEach { testParcelable(TestData(it)) } 32 | } 33 | 34 | @Test 35 | fun nullableOffsetDateTimeParceler() { 36 | (testOffsetDateTimes + listOf(null)).forEach { testParcelable(TestNullableData(it)) } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /extensions/parcelize/src/androidTest/kotlin/OffsetTimeTest.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.parcelize 2 | 3 | import android.os.Parcelable 4 | import io.islandtime.OffsetTime 5 | import io.islandtime.UtcOffset 6 | import io.islandtime.parcelize.test.testParcelable 7 | import kotlinx.parcelize.Parcelize 8 | import kotlinx.parcelize.TypeParceler 9 | import org.junit.Test 10 | 11 | class OffsetOffsetTimeTest { 12 | @Parcelize 13 | @TypeParceler 14 | data class TestData(val offsetTime: OffsetTime) : Parcelable 15 | 16 | @Parcelize 17 | @TypeParceler 18 | data class TestNullableData(val offsetTime: OffsetTime?) : Parcelable 19 | 20 | private val testOffsetTimes = listOf( 21 | OffsetTime.MIN, 22 | OffsetTime.MAX, 23 | OffsetTime(1, 2, 3, 4, UtcOffset.MIN), 24 | OffsetTime(1, 2, 3, 4, UtcOffset.MAX), 25 | OffsetTime(1, 2, 3, 4, UtcOffset.ZERO) 26 | ) 27 | 28 | @Test 29 | fun offsetTimeParceler() { 30 | testOffsetTimes.forEach { testParcelable(TestData(it)) } 31 | } 32 | 33 | @Test 34 | fun nullableOffsetTimeParceler() { 35 | (testOffsetTimes + listOf(null)).forEach { testParcelable(TestNullableData(it)) } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /extensions/parcelize/src/androidTest/kotlin/PeriodTest.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.parcelize 2 | 3 | import android.os.Parcelable 4 | import io.islandtime.parcelize.test.testParcelable 5 | import io.islandtime.measures.* 6 | import kotlinx.parcelize.Parcelize 7 | import kotlinx.parcelize.TypeParceler 8 | import org.junit.Test 9 | 10 | class PeriodTest { 11 | @Parcelize 12 | @TypeParceler 13 | data class TestData(val period: Period) : Parcelable 14 | 15 | @Parcelize 16 | @TypeParceler 17 | data class TestNullableData(val period: Period?) : Parcelable 18 | 19 | private val testPeriods = listOf( 20 | periodOf(Int.MIN_VALUE.years, Int.MIN_VALUE.months, Int.MIN_VALUE.days), 21 | periodOf(Int.MAX_VALUE.years, Int.MAX_VALUE.months, Int.MAX_VALUE.days), 22 | Period.ZERO, 23 | periodOf(1.years), 24 | periodOf(1.months), 25 | periodOf(1.days) 26 | ) 27 | 28 | @Test 29 | fun periodParceler() { 30 | testPeriods.forEach { testParcelable(TestData(it)) } 31 | } 32 | 33 | @Test 34 | fun nullablePeriodParceler() { 35 | (testPeriods + listOf(null)).forEach { testParcelable(TestNullableData(it)) } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /extensions/parcelize/src/androidTest/kotlin/TimeTest.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.parcelize 2 | 3 | import android.os.Parcelable 4 | import io.islandtime.Time 5 | import io.islandtime.parcelize.test.testParcelable 6 | import kotlinx.parcelize.Parcelize 7 | import kotlinx.parcelize.TypeParceler 8 | import org.junit.Test 9 | 10 | class TimeTest { 11 | @Parcelize 12 | @TypeParceler 13 | data class TestData(val time: Time) : Parcelable 14 | 15 | @Parcelize 16 | @TypeParceler 17 | data class TestNullableData(val time: Time?) : Parcelable 18 | 19 | private val testTimes = listOf( 20 | Time.MIN, 21 | Time.MAX, 22 | Time(1, 2, 3, 4) 23 | ) 24 | 25 | @Test 26 | fun timeParceler() { 27 | testTimes.forEach { testParcelable(TestData(it)) } 28 | } 29 | 30 | @Test 31 | fun nullableTimeParceler() { 32 | (testTimes + listOf(null)).forEach { testParcelable(TestNullableData(it)) } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /extensions/parcelize/src/androidTest/kotlin/TimeZoneTest.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.parcelize 2 | 3 | import android.os.Parcelable 4 | import io.islandtime.TimeZone 5 | import io.islandtime.parcelize.test.testParcelable 6 | import kotlinx.parcelize.Parcelize 7 | import kotlinx.parcelize.TypeParceler 8 | import org.junit.Test 9 | 10 | class TimeZoneTest { 11 | @Parcelize 12 | @TypeParceler 13 | data class TestData(val timeZone: TimeZone) : Parcelable 14 | 15 | @Parcelize 16 | @TypeParceler 17 | data class TestNullableData(val timeZone: TimeZone?) : Parcelable 18 | 19 | private val testTimeZones = listOf( 20 | TimeZone.UTC, 21 | TimeZone("-04:00"), 22 | TimeZone("America/New_York"), 23 | TimeZone("America/Denver") 24 | ) 25 | 26 | @Test 27 | fun timeZoneParceler() { 28 | testTimeZones.forEach { testParcelable(TestData(it)) } 29 | } 30 | 31 | @Test 32 | fun nullableTimeZoneParceler() { 33 | (testTimeZones + listOf(null)).forEach { testParcelable(TestNullableData(it)) } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /extensions/parcelize/src/androidTest/kotlin/YearMonthTest.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.parcelize 2 | 3 | import android.os.Parcelable 4 | import io.islandtime.YearMonth 5 | import io.islandtime.Month 6 | import io.islandtime.parcelize.test.testParcelable 7 | import kotlinx.parcelize.Parcelize 8 | import kotlinx.parcelize.TypeParceler 9 | import org.junit.Test 10 | 11 | class YearMonthTest { 12 | @Parcelize 13 | @TypeParceler 14 | data class TestData(val yearMonth: YearMonth) : Parcelable 15 | 16 | @Parcelize 17 | @TypeParceler 18 | data class TestNullableData(val yearMonth: YearMonth?) : Parcelable 19 | 20 | private val testYearMonths = listOf( 21 | YearMonth.MIN, 22 | YearMonth.MAX, 23 | YearMonth(2019, Month.DECEMBER) 24 | ) 25 | 26 | @Test 27 | fun yearMonthParceler() { 28 | testYearMonths.forEach { testParcelable(TestData(it)) } 29 | } 30 | 31 | @Test 32 | fun nullableYearMonthParceler() { 33 | (testYearMonths + listOf(null)).forEach { testParcelable(TestNullableData(it)) } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /extensions/parcelize/src/androidTest/kotlin/ZonedDateTimeIntervalTest.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.parcelize 2 | 3 | import android.os.Parcelable 4 | import io.islandtime.IslandTime 5 | import io.islandtime.TimeZone 6 | import io.islandtime.ZonedDateTime 7 | import io.islandtime.parcelize.test.testParcelable 8 | import io.islandtime.ranges.ZonedDateTimeInterval 9 | import io.islandtime.ranges.until 10 | import kotlinx.parcelize.Parcelize 11 | import kotlinx.parcelize.TypeParceler 12 | import org.junit.AfterClass 13 | import org.junit.BeforeClass 14 | import org.junit.Test 15 | 16 | class ZonedDateTimeIntervalRangeTest { 17 | @Parcelize 18 | @TypeParceler 19 | data class TestData(val zonedDateTimeInterval: ZonedDateTimeInterval) : Parcelable 20 | 21 | private val testZone = TimeZone("America/New_York") 22 | 23 | private val testZonedDateTimeIntervals = listOf( 24 | ZonedDateTimeInterval.UNBOUNDED, 25 | ZonedDateTimeInterval.EMPTY, 26 | ZonedDateTime(2019, 3, 3, 1, 2, 3, testZone) until 27 | ZonedDateTime(2019, 15, 5, 5, 6, 7, testZone) 28 | ) 29 | 30 | @Test 31 | fun zonedDateTimeIntervalParceler() { 32 | testZonedDateTimeIntervals.forEach { testParcelable(TestData(it)) } 33 | } 34 | 35 | companion object { 36 | @JvmStatic 37 | @BeforeClass 38 | fun setUp() { 39 | IslandTime.reset() 40 | } 41 | 42 | @JvmStatic 43 | @AfterClass 44 | fun tearDown() { 45 | IslandTime.reset() 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /extensions/parcelize/src/androidTest/kotlin/ZonedDateTimeTest.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.parcelize 2 | 3 | import android.os.Parcelable 4 | import io.islandtime.* 5 | import io.islandtime.parcelize.test.testParcelable 6 | import kotlinx.parcelize.Parcelize 7 | import kotlinx.parcelize.TypeParceler 8 | import org.junit.AfterClass 9 | import org.junit.BeforeClass 10 | import org.junit.Test 11 | 12 | class ZonedDateTimeTimeTest { 13 | @Parcelize 14 | @TypeParceler 15 | data class TestData(val zonedDateTime: ZonedDateTime) : Parcelable 16 | 17 | @Parcelize 18 | @TypeParceler 19 | data class TestNullableData(val zonedDateTime: ZonedDateTime?) : Parcelable 20 | 21 | private val testZonedDateTimes = listOf( 22 | DateTime.MIN at TimeZone.UTC, 23 | DateTime.MIN at TimeZone("America/New_York"), 24 | DateTime.MAX at TimeZone.UTC, 25 | DateTime.MAX at TimeZone("America/New_York"), 26 | DateTime(2019, Month.DECEMBER, 5, 1, 2, 3, 4) 27 | at TimeZone("America/Denver") 28 | ) 29 | 30 | @Test 31 | fun zonedDateTimeParceler() { 32 | testZonedDateTimes.forEach { testParcelable(TestData(it)) } 33 | } 34 | 35 | @Test 36 | fun nullableZonedDateTimeParceler() { 37 | (testZonedDateTimes + listOf(null)).forEach { testParcelable(TestNullableData(it)) } 38 | } 39 | 40 | companion object { 41 | @JvmStatic 42 | @BeforeClass 43 | fun setUp() { 44 | IslandTime.reset() 45 | } 46 | 47 | @JvmStatic 48 | @AfterClass 49 | fun tearDown() { 50 | IslandTime.reset() 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /extensions/parcelize/src/androidTest/kotlin/test/TestParcelable.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.parcelize.test 2 | 3 | import android.os.Build 4 | import android.os.Bundle 5 | import android.os.Parcel 6 | import android.os.Parcelable 7 | import com.google.common.truth.Truth.assertThat 8 | 9 | inline fun testParcelable(parcelable: T) { 10 | val inBundle = Bundle().apply { putParcelable("data", parcelable) } 11 | 12 | val outBundle = withParcel { 13 | writeBundle(inBundle) 14 | setDataPosition(0) 15 | readBundle(T::class.java.classLoader)!! 16 | } 17 | 18 | val result = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 19 | outBundle.getParcelable("data", T::class.java) 20 | } else { 21 | @Suppress("DEPRECATION") 22 | outBundle.getParcelable("data") 23 | } 24 | 25 | assertThat(result).isEqualTo(parcelable) 26 | } 27 | 28 | inline fun withParcel(block: Parcel.() -> T): T = Parcel.obtain().run { 29 | val returnValue = block() 30 | recycle() 31 | returnValue 32 | } 33 | -------------------------------------------------------------------------------- /extensions/parcelize/src/main/kotlin/Date.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.parcelize 2 | 3 | import android.os.Parcel 4 | import io.islandtime.Date 5 | import kotlinx.parcelize.Parceler 6 | 7 | object DateParceler : Parceler { 8 | override fun create(parcel: Parcel): Date = parcel.readDate() 9 | 10 | override fun Date.write(parcel: Parcel, flags: Int) { 11 | parcel.writeDate(this) 12 | } 13 | } 14 | 15 | object NullableDateParceler : Parceler { 16 | override fun create(parcel: Parcel): Date? { 17 | return when (val year = parcel.readInt()) { 18 | Int.MIN_VALUE -> null 19 | else -> Date(year, parcel.readByte().toInt(), parcel.readByte().toInt()) 20 | } 21 | } 22 | 23 | override fun Date?.write(parcel: Parcel, flags: Int) { 24 | if (this == null) { 25 | parcel.writeInt(Int.MIN_VALUE) 26 | } else { 27 | parcel.writeDate(this) 28 | } 29 | } 30 | } 31 | 32 | internal fun Parcel.readDate(): Date { 33 | return Date(readInt(), readByte().toInt(), readByte().toInt()) 34 | } 35 | 36 | internal fun Parcel.writeDate(date: Date) { 37 | writeInt(date.year) 38 | writeByte(date.monthNumber.toByte()) 39 | writeByte(date.dayOfMonth.toByte()) 40 | } 41 | -------------------------------------------------------------------------------- /extensions/parcelize/src/main/kotlin/DateRange.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.parcelize 2 | 3 | import android.os.Parcel 4 | import io.islandtime.ranges.DateRange 5 | import kotlinx.parcelize.Parceler 6 | 7 | object DateRangeParceler : Parceler { 8 | override fun create(parcel: Parcel): DateRange = DateRange(parcel.readDate(), parcel.readDate()) 9 | 10 | override fun DateRange.write(parcel: Parcel, flags: Int) { 11 | parcel.writeDate(start) 12 | parcel.writeDate(endInclusive) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /extensions/parcelize/src/main/kotlin/DateTime.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.parcelize 2 | 3 | import android.os.Parcel 4 | import io.islandtime.DateTime 5 | import kotlinx.parcelize.Parceler 6 | 7 | object DateTimeParceler : Parceler { 8 | override fun create(parcel: Parcel): DateTime = parcel.readDateTime() 9 | 10 | override fun DateTime.write(parcel: Parcel, flags: Int) { 11 | parcel.writeDateTime(this) 12 | } 13 | } 14 | 15 | object NullableDateTimeParceler : Parceler { 16 | override fun create(parcel: Parcel): DateTime? { 17 | return when (val year = parcel.readInt()) { 18 | Int.MIN_VALUE -> null 19 | else -> DateTime( 20 | year, 21 | parcel.readByte().toInt(), 22 | parcel.readByte().toInt(), 23 | parcel.readByte().toInt(), 24 | parcel.readByte().toInt(), 25 | parcel.readByte().toInt(), 26 | parcel.readInt() 27 | ) 28 | } 29 | } 30 | 31 | override fun DateTime?.write(parcel: Parcel, flags: Int) { 32 | if (this == null) { 33 | parcel.writeInt(Int.MIN_VALUE) 34 | } else { 35 | parcel.writeDateTime(this) 36 | } 37 | } 38 | } 39 | 40 | internal fun Parcel.readDateTime(): DateTime { 41 | return DateTime(readDate(), readTime()) 42 | } 43 | 44 | internal fun Parcel.writeDateTime(dateTime: DateTime) { 45 | writeDate(dateTime.date) 46 | writeTime(dateTime.time) 47 | } 48 | -------------------------------------------------------------------------------- /extensions/parcelize/src/main/kotlin/DateTimeInterval.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.parcelize 2 | 3 | import android.os.Parcel 4 | import io.islandtime.ranges.DateTimeInterval 5 | import kotlinx.parcelize.Parceler 6 | 7 | object DateTimeIntervalParceler: Parceler { 8 | override fun create(parcel: Parcel): DateTimeInterval { 9 | return DateTimeInterval(parcel.readDateTime(), parcel.readDateTime()) 10 | } 11 | 12 | override fun DateTimeInterval.write(parcel: Parcel, flags: Int) { 13 | parcel.writeDateTime(start) 14 | parcel.writeDateTime(endExclusive) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /extensions/parcelize/src/main/kotlin/Duration.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.parcelize 2 | 3 | import android.os.Parcel 4 | import io.islandtime.measures.Duration 5 | import io.islandtime.measures.durationOf 6 | import io.islandtime.measures.nanoseconds 7 | import io.islandtime.measures.seconds 8 | import kotlinx.parcelize.Parceler 9 | 10 | object DurationParceler : Parceler { 11 | override fun create(parcel: Parcel): Duration { 12 | return durationOf(parcel.readLong().seconds, parcel.readInt().nanoseconds) 13 | } 14 | 15 | override fun Duration.write(parcel: Parcel, flags: Int) { 16 | parcel.writeLong(seconds.value) 17 | parcel.writeInt(nanosecondAdjustment.toInt()) 18 | } 19 | } 20 | 21 | object NullableDurationParceler : Parceler { 22 | override fun create(parcel: Parcel): Duration? { 23 | return when { 24 | parcel.readByte() == NULL_VALUE -> null 25 | else -> durationOf(parcel.readLong().seconds, parcel.readInt().nanoseconds) 26 | } 27 | } 28 | 29 | override fun Duration?.write(parcel: Parcel, flags: Int) { 30 | if (this == null) { 31 | parcel.writeByte(NULL_VALUE) 32 | } else { 33 | parcel.writeByte(NON_NULL_VALUE) 34 | parcel.writeLong(seconds.value) 35 | parcel.writeInt(nanosecondAdjustment.toInt()) 36 | } 37 | } 38 | 39 | private const val NON_NULL_VALUE = 0.toByte() 40 | private const val NULL_VALUE = 1.toByte() 41 | } 42 | -------------------------------------------------------------------------------- /extensions/parcelize/src/main/kotlin/Instant.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.parcelize 2 | 3 | import android.os.Parcel 4 | import io.islandtime.Instant 5 | import kotlinx.parcelize.Parceler 6 | 7 | object InstantParceler : Parceler { 8 | override fun create(parcel: Parcel): Instant = parcel.readInstant() 9 | 10 | override fun Instant.write(parcel: Parcel, flags: Int) { 11 | parcel.writeInstant(this) 12 | } 13 | } 14 | 15 | object NullableInstantParceler : Parceler { 16 | override fun create(parcel: Parcel): Instant? { 17 | return when (val second = parcel.readLong()) { 18 | Long.MIN_VALUE -> null 19 | else -> Instant.fromSecondOfUnixEpoch(second, parcel.readInt()) 20 | } 21 | } 22 | 23 | override fun Instant?.write(parcel: Parcel, flags: Int) { 24 | if (this == null) { 25 | parcel.writeLong(Long.MIN_VALUE) 26 | } else { 27 | parcel.writeInstant(this) 28 | } 29 | } 30 | } 31 | 32 | internal fun Parcel.readInstant(): Instant { 33 | return Instant.fromSecondOfUnixEpoch(readLong(), readInt()) 34 | } 35 | 36 | internal fun Parcel.writeInstant(instant: Instant) { 37 | writeLong(instant.secondOfUnixEpoch) 38 | writeInt(instant.nanosecond) 39 | } 40 | -------------------------------------------------------------------------------- /extensions/parcelize/src/main/kotlin/InstantInterval.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.parcelize 2 | 3 | import android.os.Parcel 4 | import io.islandtime.ranges.InstantInterval 5 | import kotlinx.parcelize.Parceler 6 | 7 | object InstantIntervalParceler : Parceler { 8 | override fun create(parcel: Parcel): InstantInterval { 9 | return InstantInterval(parcel.readInstant(), parcel.readInstant()) 10 | } 11 | 12 | override fun InstantInterval.write(parcel: Parcel, flags: Int) { 13 | parcel.writeInstant(start) 14 | parcel.writeInstant(endExclusive) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /extensions/parcelize/src/main/kotlin/OffsetDateTime.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.parcelize 2 | 3 | import android.os.Parcel 4 | import io.islandtime.DateTime 5 | import io.islandtime.UtcOffset 6 | import io.islandtime.OffsetDateTime 7 | import io.islandtime.measures.seconds 8 | import kotlinx.parcelize.Parceler 9 | 10 | object OffsetDateTimeParceler : Parceler { 11 | override fun create(parcel: Parcel): OffsetDateTime = parcel.readOffsetDateTime() 12 | 13 | override fun OffsetDateTime.write(parcel: Parcel, flags: Int) { 14 | parcel.writeOffsetDateTime(this) 15 | } 16 | } 17 | 18 | object NullableOffsetDateTimeParceler : Parceler { 19 | override fun create(parcel: Parcel): OffsetDateTime? { 20 | return when (val year = parcel.readInt()) { 21 | Int.MIN_VALUE -> null 22 | else -> OffsetDateTime( 23 | DateTime( 24 | year, 25 | parcel.readByte().toInt(), 26 | parcel.readByte().toInt(), 27 | parcel.readByte().toInt(), 28 | parcel.readByte().toInt(), 29 | parcel.readByte().toInt(), 30 | parcel.readInt() 31 | ), 32 | UtcOffset.fromTotalSeconds(parcel.readInt()) 33 | ) 34 | } 35 | } 36 | 37 | override fun OffsetDateTime?.write(parcel: Parcel, flags: Int) { 38 | if (this == null) { 39 | parcel.writeInt(Int.MIN_VALUE) 40 | } else { 41 | parcel.writeOffsetDateTime(this) 42 | } 43 | } 44 | } 45 | 46 | internal fun Parcel.readOffsetDateTime(): OffsetDateTime { 47 | return OffsetDateTime(readDateTime(), UtcOffset(readInt().seconds)) 48 | } 49 | 50 | internal fun Parcel.writeOffsetDateTime(offsetDateTime: OffsetDateTime) { 51 | writeDateTime(offsetDateTime.dateTime) 52 | writeInt(offsetDateTime.offset.totalSecondsValue) 53 | } 54 | -------------------------------------------------------------------------------- /extensions/parcelize/src/main/kotlin/OffsetDateTimeInterval.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.parcelize 2 | 3 | import android.os.Parcel 4 | import io.islandtime.ranges.OffsetDateTimeInterval 5 | import kotlinx.parcelize.Parceler 6 | 7 | object OffsetDateTimeIntervalParceler : Parceler { 8 | override fun create(parcel: Parcel): OffsetDateTimeInterval { 9 | return OffsetDateTimeInterval(parcel.readOffsetDateTime(), parcel.readOffsetDateTime()) 10 | } 11 | 12 | override fun OffsetDateTimeInterval.write(parcel: Parcel, flags: Int) { 13 | parcel.writeOffsetDateTime(start) 14 | parcel.writeOffsetDateTime(endExclusive) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /extensions/parcelize/src/main/kotlin/OffsetTime.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.parcelize 2 | 3 | import android.os.Parcel 4 | import io.islandtime.OffsetTime 5 | import io.islandtime.UtcOffset 6 | import io.islandtime.measures.seconds 7 | import kotlinx.parcelize.Parceler 8 | 9 | object OffsetTimeParceler : Parceler { 10 | override fun create(parcel: Parcel): OffsetTime { 11 | return OffsetTime( 12 | parcel.readByte().toInt(), 13 | parcel.readByte().toInt(), 14 | parcel.readByte().toInt(), 15 | parcel.readInt(), 16 | UtcOffset.fromTotalSeconds(parcel.readInt()) 17 | ) 18 | } 19 | 20 | override fun OffsetTime.write(parcel: Parcel, flags: Int) { 21 | parcel.writeByte(hour.toByte()) 22 | parcel.writeByte(minute.toByte()) 23 | parcel.writeByte(second.toByte()) 24 | parcel.writeInt(nanosecond) 25 | parcel.writeInt(offset.totalSecondsValue) 26 | } 27 | } 28 | 29 | object NullableOffsetTimeParceler : Parceler { 30 | override fun create(parcel: Parcel): OffsetTime? { 31 | return when (val hour = parcel.readByte()) { 32 | Byte.MIN_VALUE -> null 33 | else -> OffsetTime( 34 | hour.toInt(), 35 | parcel.readByte().toInt(), 36 | parcel.readByte().toInt(), 37 | parcel.readInt(), 38 | UtcOffset.fromTotalSeconds(parcel.readInt()) 39 | ) 40 | } 41 | } 42 | 43 | override fun OffsetTime?.write(parcel: Parcel, flags: Int) { 44 | if (this == null) { 45 | parcel.writeByte(Byte.MIN_VALUE) 46 | } else { 47 | parcel.writeByte(hour.toByte()) 48 | parcel.writeByte(minute.toByte()) 49 | parcel.writeByte(second.toByte()) 50 | parcel.writeInt(nanosecond) 51 | parcel.writeInt(offset.totalSecondsValue) 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /extensions/parcelize/src/main/kotlin/Period.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.parcelize 2 | 3 | import android.os.Parcel 4 | import io.islandtime.measures.* 5 | import kotlinx.parcelize.Parceler 6 | 7 | object PeriodParceler : Parceler { 8 | override fun create(parcel: Parcel): Period { 9 | return periodOf( 10 | parcel.readLong().years, 11 | parcel.readLong().months, 12 | parcel.readLong().days 13 | ) 14 | } 15 | 16 | override fun Period.write(parcel: Parcel, flags: Int) { 17 | parcel.writeLong(years.value) 18 | parcel.writeLong(months.value) 19 | parcel.writeLong(days.value) 20 | } 21 | } 22 | 23 | object NullablePeriodParceler : Parceler { 24 | override fun create(parcel: Parcel): Period? { 25 | return when { 26 | parcel.readByte() == NULL_VALUE -> null 27 | else -> periodOf( 28 | parcel.readLong().years, 29 | parcel.readLong().months, 30 | parcel.readLong().days 31 | ) 32 | } 33 | } 34 | 35 | override fun Period?.write(parcel: Parcel, flags: Int) { 36 | if (this == null) { 37 | parcel.writeByte(NULL_VALUE) 38 | } else { 39 | parcel.writeByte(NON_NULL_VALUE) 40 | parcel.writeLong(years.value) 41 | parcel.writeLong(months.value) 42 | parcel.writeLong(days.value) 43 | } 44 | } 45 | 46 | private const val NON_NULL_VALUE = 0.toByte() 47 | private const val NULL_VALUE = 1.toByte() 48 | } 49 | -------------------------------------------------------------------------------- /extensions/parcelize/src/main/kotlin/Time.kt: -------------------------------------------------------------------------------- 1 | package io.islandtime.parcelize 2 | 3 | import android.os.Parcel 4 | import io.islandtime.Time 5 | import kotlinx.parcelize.Parceler 6 | 7 | object TimeParceler : Parceler