├── .gitignore ├── README.md ├── build.gradle.kts ├── gradle.properties ├── gradle ├── pom.gradle ├── publish.gradle └── wrapper │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle.kts ├── src ├── commonMain │ └── kotlin │ │ └── com │ │ └── lightningkite │ │ └── lokalize │ │ ├── Expect.kt │ │ ├── Locale.kt │ │ ├── location │ │ ├── Geohash.kt │ │ ├── GeohashCoverage.kt │ │ └── World.kt │ │ ├── time │ │ ├── Date.kt │ │ ├── DateTime.kt │ │ ├── DayOfWeek.kt │ │ ├── DaysOfWeek.kt │ │ ├── Duration.kt │ │ ├── Month.kt │ │ ├── ShortDuration.kt │ │ ├── Time.kt │ │ ├── TimeConstants.kt │ │ ├── TimeStamp.kt │ │ ├── TimeUnit.kt │ │ ├── Year.kt │ │ ├── YearAndDayInYear.kt │ │ └── expect.kt │ │ └── units │ │ ├── Measurement.kt │ │ └── test.kt ├── commonTest │ └── kotlin │ │ └── com │ │ └── lightningkite │ │ └── lokalize │ │ ├── location │ │ ├── GeohashCoverageTest.kt │ │ └── GeohashTest.kt │ │ └── time │ │ └── Iso8601Test.kt ├── iosMain │ └── kotlin │ │ └── com │ │ └── lightningkite │ │ └── lokalize │ │ ├── default.kt │ │ └── time │ │ └── default.kt ├── jsMain │ └── kotlin │ │ └── com │ │ └── lightningkite │ │ └── lokalize │ │ ├── default.kt │ │ └── time │ │ └── default.kt ├── jvmMain │ └── kotlin │ │ └── com │ │ └── lightningkite │ │ └── lokalize │ │ ├── default.kt │ │ └── time │ │ ├── JavaExt.kt │ │ └── default.kt ├── mingwX64Main │ └── kotlin │ │ └── com │ │ └── lightningkite │ │ └── lokalize │ │ ├── default.kt │ │ └── time │ │ └── default.kt └── posixMain │ └── kotlin │ └── com │ └── lightningkite │ └── lokalize │ ├── default.kt │ └── time │ └── default.kt └── versions.properties /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Gradle template 3 | .gradle 4 | /build/ 5 | 6 | *.iml 7 | 8 | # Ignore Gradle GUI config 9 | gradle-app.setting 10 | 11 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 12 | !gradle-wrapper.jar 13 | 14 | # Cache of project 15 | .gradletasknamecache 16 | 17 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 18 | # gradle/wrapper/gradle-wrapper.properties 19 | ### JetBrains template 20 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 21 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 22 | 23 | # User-specific stuff 24 | .idea/**/workspace.xml 25 | .idea/**/tasks.xml 26 | .idea/**/usage.statistics.xml 27 | .idea/**/dictionaries 28 | .idea/**/shelf 29 | 30 | # Sensitive or high-churn files 31 | .idea/**/dataSources/ 32 | .idea/**/dataSources.ids 33 | .idea/**/dataSources.local.xml 34 | .idea/**/sqlDataSources.xml 35 | .idea/**/dynamic.xml 36 | .idea/**/uiDesigner.xml 37 | .idea/**/dbnavigator.xml 38 | 39 | # Gradle 40 | .idea/**/gradle.xml 41 | .idea/**/libraries 42 | 43 | # Gradle and Maven with auto-import 44 | # When using Gradle or Maven with auto-import, you should exclude module files, 45 | # since they will be recreated, and may cause churn. Uncomment if using 46 | # auto-import. 47 | # .idea/modules.xml 48 | # .idea/*.iml 49 | # .idea/modules 50 | 51 | # CMake 52 | cmake-build-*/ 53 | 54 | # Mongo Explorer plugin 55 | .idea/**/mongoSettings.xml 56 | 57 | # File-based project format 58 | *.iws 59 | 60 | # IntelliJ 61 | out/ 62 | 63 | # mpeltonen/sbt-idea plugin 64 | .idea_modules/ 65 | 66 | # JIRA plugin 67 | atlassian-ide-plugin.xml 68 | 69 | # Cursive Clojure plugin 70 | .idea/replstate.xml 71 | 72 | # Crashlytics plugin (for Android Studio and IntelliJ) 73 | com_crashlytics_export_strings.xml 74 | crashlytics.properties 75 | crashlytics-build.properties 76 | fabric.properties 77 | 78 | # Editor-based Rest Client 79 | .idea/httpRequests 80 | ### Kotlin template 81 | # Compiled class file 82 | *.class 83 | 84 | # Log file 85 | *.log 86 | 87 | # BlueJ files 88 | *.ctxt 89 | 90 | # Mobile Tools for Java (J2ME) 91 | .mtj.tmp/ 92 | 93 | # Package Files # 94 | *.jar 95 | *.war 96 | *.nar 97 | *.ear 98 | *.zip 99 | *.tar.gz 100 | *.rar 101 | 102 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 103 | hs_err_pid* 104 | 105 | .idea -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lokalize (LK) 2 | 3 | Maven: [ ![Download](https://api.bintray.com/packages/lightningkite/com.lightningkite.krosslin/lokalize/images/download.svg) ](https://bintray.com/lightningkite/com.lightningkite.krosslin/lokalize/_latestVersion) 4 | 5 | Tools for showing data localized across platforms. 6 | 7 | Especially implements time components. 8 | 9 | ``` 10 | repositories { 11 | maven { url 'https://dl.bintray.com/lightningkite/com.lightningkite.krosslin' } 12 | ... 13 | } 14 | ... 15 | dependencies { 16 | ... 17 | //Depending on the version you need 18 | api "com.lightningkite:lokalize-metadata:${lokalizeVersion}" 19 | api "com.lightningkite:lokalize-jvm:${lokalizeVersion}" 20 | api "com.lightningkite:lokalize-js:${lokalizeVersion}" 21 | api "com.lightningkite:lokalize-iosarm64:${lokalizeVersion}" 22 | api "com.lightningkite:lokalize-iosx64:${lokalizeVersion}" 23 | and more! 24 | } 25 | ``` 26 | 27 | ## Features 28 | 29 | - `Locale.default`, which gives you a default locale. 30 | - `Locale.language`, which gives you a language code. 31 | - `Locale.languageVariant`, which gives you a language variant code. 32 | - `Locale.getTimeOffsetMilliseconds()`, which gives you the time zone offest. 33 | - `Locale.renderNumber()`, which renders a number to a string. 34 | - `Locale.renderDate()`, which renders a date to a string. 35 | - `Locale.renderTime()`, which renders a time to a string. 36 | - `Locale.renderDateTime()`, which renders a date time to a string. 37 | - `Locale.renderTimeStamp()`, which renders a time stamp. 38 | - `Date`, which only contains a date 39 | - `Time`, which only contains a time in the day 40 | - `DateTime`, which combines both 41 | - `TimeStamp`, which is always in UTC 42 | - `TimeStamp.now()`, which gets the current time 43 | - `Duration`, which represents a duration in milliseconds 44 | - `ShortDuration`, which represents a finer duration in nanoseconds 45 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.lightningkite.konvenience.gradle.* 2 | import java.util.Properties 3 | 4 | plugins { 5 | kotlin("multiplatform") 6 | `maven-publish` 7 | } 8 | 9 | buildscript { 10 | repositories { 11 | mavenLocal() 12 | maven("https://dl.bintray.com/lightningkite/com.lightningkite.krosslin") 13 | } 14 | dependencies { 15 | classpath("com.lightningkite:konvenience:+") 16 | } 17 | } 18 | apply(plugin = "com.lightningkite.konvenience") 19 | 20 | repositories { 21 | mavenLocal() 22 | mavenCentral() 23 | maven("https://dl.bintray.com/lightningkite/com.lightningkite.krosslin") 24 | maven("https://dl.bintray.com/kotlin/kotlin-eap") 25 | maven("https://dl.bintray.com/kotlin/kotlin-dev") 26 | } 27 | 28 | val versions = Properties().apply { 29 | load(project.file("versions.properties").inputStream()) 30 | } 31 | 32 | group = "com.lightningkite" 33 | version = versions.getProperty(project.name) 34 | 35 | kotlin { 36 | 37 | sources(tryTargets = KTarget.allExceptAndroid - KTarget.wasm32) { 38 | main { 39 | dependency(standardLibrary) 40 | dependency(projectOrMavenDashPlatform("com.lightningkite", "kommon", versions.getProperty("kommon"))) 41 | } 42 | test { 43 | dependency(testing) 44 | dependency(testingAnnotations) 45 | } 46 | isIos.sources {} 47 | isJs.sources {} 48 | isJvm.sources {} 49 | isMingwX64.sources {} 50 | (isPosix and !isIos).sources("posix") {} 51 | } 52 | } 53 | 54 | publishing { 55 | doNotPublishMetadata() 56 | repositories { 57 | bintray( 58 | project = project, 59 | organization = "lightningkite", 60 | repository = "com.lightningkite.krosslin" 61 | ) 62 | } 63 | 64 | appendToPoms { 65 | github("lightningkite", project.name) 66 | licenseMIT() 67 | developers { 68 | developer { 69 | id.set("UnknownJoe796") 70 | name.set("Joseph Ivie") 71 | email.set("joseph@lightningkite.com") 72 | timezone.set("America/Denver") 73 | roles.set(listOf("architect", "developer")) 74 | organization.set("Lightning Kite") 75 | organizationUrl.set("http://www.lightningkite.com") 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | websiteUrl=https://github.com/lightningkite/lokalize 3 | vcsUrl=https://github.com/lightningkite/lokalize.git 4 | issuesUrl=https://github.com/lightningkite/lokalize/issues 5 | bintrayOrganization=lightningkite 6 | bintrayRepository=com.lightningkite.krosslin 7 | bintrayLicense=MIT 8 | bintrayPublish=false 9 | -------------------------------------------------------------------------------- /gradle/pom.gradle: -------------------------------------------------------------------------------- 1 | def pomConfig = { 2 | licenses { 3 | license { 4 | name "MIT License" 5 | url "http://www.opensource.org/licenses/mit-license.php" 6 | distribution "repo" 7 | } 8 | } 9 | developers { 10 | developer { 11 | id "LightningKite" 12 | name "Lightning Kite" 13 | organization "lightningkite" 14 | organizationUrl "http://www.lightningkite.com" 15 | } 16 | } 17 | 18 | scm { 19 | url vcsUrl 20 | } 21 | } 22 | 23 | project.ext.configureMavenCentralMetadata = { pom -> 24 | def root = asNode() 25 | root.appendNode('name', project.name) 26 | root.appendNode('description', project.description) 27 | root.appendNode('url', project.vcsUrl) 28 | root.children().last() + pomConfig 29 | } 30 | 31 | project.ext.configurePom = pomConfig 32 | -------------------------------------------------------------------------------- /gradle/publish.gradle: -------------------------------------------------------------------------------- 1 | import java.nio.file.Files 2 | import java.nio.file.Paths 3 | 4 | // Configures publishing of Maven artifacts to Bintray 5 | 6 | apply plugin: 'maven' 7 | apply plugin: 'maven-publish' 8 | apply plugin: 'com.jfrog.bintray' 9 | apply from: project.rootProject.file('gradle/pom.gradle') 10 | 11 | // Load `local.properties` file, if it exists. You can put your bintrayUser and bintrayApiKey values there, that file is ignored by git 12 | if (Files.exists(Paths.get("$project.rootDir/local.properties"))) { 13 | def localProperties = new Properties() 14 | localProperties.load(new FileInputStream("$project.rootDir/local.properties")) 15 | localProperties.each { prop -> 16 | println("Setting ${prop.key} to ${prop.value}") 17 | project.ext.set(prop.key, prop.value) 18 | } 19 | println("Properties pulled from: $project.rootDir/local.properties") 20 | } 21 | if (Files.exists(Paths.get("$project.rootDir/../local.properties"))) { 22 | def localProperties = new Properties() 23 | localProperties.load(new FileInputStream("$project.rootDir/../local.properties")) 24 | localProperties.each { prop -> 25 | println("Setting ${prop.key} to ${prop.value}") 26 | project.ext.set(prop.key, prop.value) 27 | } 28 | println("Properties pulled from: $project.rootDir/../local.properties") 29 | } 30 | if (Files.exists(Paths.get("$project.rootDir/../../local.properties"))) { 31 | def localProperties = new Properties() 32 | localProperties.load(new FileInputStream("$project.rootDir/../../local.properties")) 33 | localProperties.each { prop -> 34 | println("Setting ${prop.key} to ${prop.value}") 35 | project.ext.set(prop.key, prop.value) 36 | } 37 | println("Properties pulled from: $project.rootDir/../../local.properties") 38 | } 39 | 40 | // Create empty jar for sources classifier to satisfy maven requirements 41 | task stubSources(type: Jar) { 42 | classifier = 'sources' 43 | } 44 | 45 | // Create empty jar for javadoc classifier to satisfy maven requirements 46 | task stubJavadoc(type: Jar) { 47 | classifier = 'javadoc' 48 | } 49 | 50 | // Configure publishing 51 | publishing { 52 | // TODO: I have no idea about this 53 | repositories { 54 | maven { 55 | url = "https://${project.bintrayOrganization}.bintray.com/${project.bintrayRepository}" 56 | } 57 | } 58 | 59 | // Process each publication we have in this project 60 | publications.all { publication -> 61 | // apply changes to pom.xml files, see pom.gradle 62 | pom.withXml(configureMavenCentralMetadata) 63 | 64 | if (publication.name == 'kotlinMultiplatform') { 65 | // for our root metadata publication, set artifactId with a package and project name 66 | publication.artifactId = "${project.name}" 67 | } else { 68 | // for targets, set artifactId with a package, project name and target name (e.g. iosX64) 69 | publication.artifactId = "${project.name}-$publication.name" 70 | } 71 | } 72 | 73 | // Patch publications with fake javadoc 74 | kotlin.targets.all { target -> 75 | def targetPublication = publications.findByName(target.name) 76 | if (targetPublication != null) { 77 | targetPublication.artifact stubJavadoc 78 | } 79 | } 80 | 81 | // Remove gradle metadata publishing from all targets which are not native 82 | kotlin.targets.all { target -> 83 | if (target.platformType.name != 'native') { 84 | try { 85 | def publication = publishing.publications[targetName] as MavenPublication 86 | if (target.platformType.name != 'native') { 87 | publication.moduleDescriptorGenerator = null 88 | } else { 89 | publication.artifact emptyJar 90 | } 91 | tasks.matching { it.name == "generateMetadataFileFor${name.capitalize()}Publication" }.all { 92 | onlyIf { false } 93 | } 94 | } catch (Exception e) { 95 | println("For target $target: ${e.message}") 96 | } 97 | } 98 | } 99 | } 100 | 101 | bintray { 102 | user = project.hasProperty('bintrayUser') ? project.property('bintrayUser') : System.getenv('BINTRAY_USER') 103 | key = project.hasProperty('bintrayApiKey') ? project.property('bintrayApiKey') : System.getenv('BINTRAY_API_KEY') 104 | publish = project.bintrayPublish 105 | override = true // for multi-platform Kotlin/Native publishing 106 | 107 | pkg { 108 | userOrg = project.bintrayOrganization 109 | repo = project.bintrayRepository 110 | name = project.name 111 | licenses = [project.bintrayLicense] 112 | vcsUrl = project.vcsUrl 113 | websiteUrl = project.websiteUrl 114 | issueTrackerUrl = project.issuesUrl 115 | version { 116 | name = project.version 117 | vcsTag = project.version 118 | released = new Date() 119 | } 120 | } 121 | } 122 | 123 | // TODO :kludge this is required for K/N publishing 124 | bintrayUpload.dependsOn publishToMavenLocal 125 | 126 | // This is for easier debugging of bintray uploading problems 127 | bintrayUpload.doFirst { 128 | publications = project.publishing.publications.findAll { 129 | !it.name.contains('-test') && it.name != 'kotlinMultiplatform' 130 | }.collect { 131 | println("Uploading artifact '$it.groupId:$it.artifactId:$it.version' from publication '$it.name'") 132 | for (art in it.artifacts) { 133 | println("ART: ${art.getFile()}") 134 | } 135 | it.name 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.5-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | mavenCentral() 5 | google() 6 | jcenter() 7 | mavenLocal() 8 | maven("https://dl.bintray.com/lightningkite/com.lightningkite.krosslin") 9 | maven("https://dl.bintray.com/kotlin/kotlin-eap") 10 | maven("https://dl.bintray.com/kotlin/kotlin-dev") 11 | } 12 | } 13 | 14 | enableFeaturePreview("GRADLE_METADATA") 15 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/lightningkite/lokalize/Expect.kt: -------------------------------------------------------------------------------- 1 | package com.lightningkite.lokalize 2 | 3 | 4 | expect val DefaultLocale: Locale -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/lightningkite/lokalize/Locale.kt: -------------------------------------------------------------------------------- 1 | package com.lightningkite.lokalize 2 | 3 | import com.lightningkite.lokalize.time.* 4 | 5 | 6 | interface Locale{ 7 | 8 | val language: String 9 | val languageVariant: String 10 | fun getTimeOffsetMilliseconds():Long 11 | fun renderNumber(value: Number, decimalPositions: Int, maxOtherPositions: Int):String 12 | fun renderDate(date: Date):String 13 | fun renderTime(time: Time):String 14 | fun renderDateTime(dateTime: DateTime):String 15 | fun renderTimeStamp(timeStamp: TimeStamp):String 16 | 17 | companion object { 18 | val default: Locale get() = DefaultLocale 19 | } 20 | } 21 | 22 | fun Locale.getTimeOffset(): Duration = Duration(getTimeOffsetMilliseconds()) 23 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/lightningkite/lokalize/location/Geohash.kt: -------------------------------------------------------------------------------- 1 | package com.lightningkite.lokalize.location 2 | 3 | import com.lightningkite.kommon.bytes.bitHigh 4 | import com.lightningkite.kommon.bytes.bitHighOff 5 | import com.lightningkite.kommon.bytes.bitHighOn 6 | import com.lightningkite.kommon.bytes.bitHighSet 7 | import kotlin.math.* 8 | 9 | 10 | inline class Geohash(val bits: Long) : Comparable { 11 | override fun compareTo(other: Geohash): Int { 12 | return bits.compareTo(other.bits) 13 | } 14 | 15 | constructor(latitude: Double, longitude: Double):this(Unit.run{ 16 | var bits = 0L 17 | 18 | var lower = -90.0 19 | var upper = 90.0 20 | for (latBit in 0..31) { 21 | val middle = (upper + lower) / 2 22 | if (latitude >= middle) { 23 | bits = bits.bitHighOn(latBit * 2 + 1) 24 | lower = middle 25 | } else { 26 | bits = bits.bitHighOff(latBit * 2 + 1) 27 | upper = middle 28 | } 29 | } 30 | 31 | lower = -180.0 32 | upper = 180.0 33 | for (lonBit in 0..31) { 34 | val middle = (upper + lower) / 2 35 | if (longitude >= middle) { 36 | bits = bits.bitHighOn(lonBit * 2) 37 | lower = middle 38 | } else { 39 | bits = bits.bitHighOff(lonBit * 2) 40 | upper = middle 41 | } 42 | } 43 | 44 | bits 45 | }) 46 | 47 | constructor(string: String):this(Unit.run{ 48 | var bits = 0L 49 | var position = 59 50 | var stringIndex = 0 51 | for (c in string) { 52 | bits = bits or fromChar(c).toLong().shl(position) 53 | position -= 5 54 | stringIndex++ 55 | if (position < 0) break 56 | } 57 | if (stringIndex < string.length) { 58 | bits = bits or fromChar(string[stringIndex]).toLong().shr(1) 59 | } 60 | bits 61 | }) 62 | 63 | companion object { 64 | val chars = charArrayOf( 65 | '0', '1', '2', '3', '4', '5', '6', '7', 66 | '8', '9', 'b', 'c', 'd', 'e', 'f', 'g', 67 | 'h', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 68 | 's', 't', 'u', 'v', 'w', 'x', 'y', 'z' 69 | ) 70 | 71 | @Suppress("NOTHING_TO_INLINE") 72 | inline fun fromChar(char: Char): Int { 73 | if (char <= '9') return char - '0' 74 | else if (char < 'j') return char - 'b' + 10 75 | else if (char < 'm') return char - 'j' + 17 76 | else if (char < 'p') return char - 'm' + 19 77 | else return char - 'p' + 21 78 | } 79 | 80 | @Deprecated("You can call the constructor directly", ReplaceWith("Geohash(latitude, longitude)")) 81 | fun fromLatLng(latitude: Double, longitude: Double): Geohash = Geohash(latitude, longitude) 82 | 83 | @Deprecated("You can call the constructor directly", ReplaceWith("Geohash(latitude, longitude)")) 84 | fun fromString(string: String): Geohash = Geohash(string) 85 | 86 | fun fromLatLongBits(latitude: Int, longitude: Int): Geohash { 87 | var bits = 0L 88 | for (index in 0..31) { 89 | if (longitude.bitHigh(index)) { 90 | bits = bits.bitHighOn(index * 2) 91 | } 92 | if (latitude.bitHigh(index)) { 93 | bits = bits.bitHighOn(index * 2 + 1) 94 | } 95 | } 96 | return Geohash(bits) 97 | } 98 | } 99 | 100 | val latitude: Double 101 | get() { 102 | var lower = -90.0 103 | var upper = 90.0 104 | for (latBit in 0..31) { 105 | val middle = (upper + lower) / 2 106 | if (bits.bitHigh(latBit * 2 + 1)) { 107 | lower = middle 108 | } else { 109 | upper = middle 110 | } 111 | } 112 | return lower 113 | } 114 | 115 | val longitude: Double 116 | get() { 117 | var lower = -180.0 118 | var upper = 180.0 119 | for (lonBit in 0..31) { 120 | val middle = (upper + lower) / 2 121 | if (bits.bitHigh(lonBit * 2)) { 122 | lower = middle 123 | } else { 124 | upper = middle 125 | } 126 | } 127 | return lower 128 | } 129 | 130 | override fun toString(): String { 131 | val builder = StringBuilder() 132 | for (i in 59 downTo 0 step 5) { 133 | builder.append(chars[bits.ushr(i).toInt().and(0x1F)]) 134 | } 135 | builder.append(chars[((bits shl 1) and 0x1F).toInt()]) 136 | return builder.toString() 137 | } 138 | 139 | val latitudeBits: Int 140 | get() { 141 | var partialBits = 0 142 | for (index in 0..31) { 143 | partialBits = partialBits.bitHighSet(index, bits.bitHigh(index * 2 + 1)) 144 | } 145 | return partialBits 146 | } 147 | 148 | val longitudeBits: Int 149 | get() { 150 | var partialBits = 0 151 | for (index in 0..31) { 152 | partialBits = partialBits.bitHighSet(index, bits.bitHigh(index * 2)) 153 | } 154 | return partialBits 155 | } 156 | 157 | fun applyLatitudeBits(latitudeBits: Int): Geohash { 158 | var newBits = bits 159 | for (index in 0..31) { 160 | newBits = newBits.bitHighSet(index * 2 + 1, latitudeBits.bitHigh(index)) 161 | } 162 | return Geohash(newBits) 163 | } 164 | 165 | fun applyLongitudeBits(longitudeBits: Int): Geohash { 166 | var newBits = bits 167 | for (index in 0..31) { 168 | newBits = newBits.bitHighSet(index * 2, longitudeBits.bitHigh(index)) 169 | } 170 | return Geohash(newBits) 171 | } 172 | 173 | fun north(bitsResolution: Int) = applyLatitudeBits(latitudeBits + (0x1 shl (31 - bitsResolution))) 174 | fun south(bitsResolution: Int) = applyLatitudeBits(latitudeBits - (0x1 shl (31 - bitsResolution))) 175 | fun east(bitsResolution: Int) = applyLongitudeBits(longitudeBits + (0x1 shl (31 - bitsResolution))) 176 | fun west(bitsResolution: Int) = applyLongitudeBits(longitudeBits - (0x1 shl (31 - bitsResolution))) 177 | 178 | fun northEast(bitsResolution: Int) = Geohash.fromLatLongBits( 179 | latitude = latitudeBits + (0x1 shl (31 - bitsResolution)), 180 | longitude = longitudeBits + (0x1 shl (31 - bitsResolution)) 181 | ) 182 | 183 | fun northWest(bitsResolution: Int) = Geohash.fromLatLongBits( 184 | latitude = latitudeBits + (0x1 shl (31 - bitsResolution)), 185 | longitude = longitudeBits - (0x1 shl (31 - bitsResolution)) 186 | ) 187 | 188 | fun southEast(bitsResolution: Int) = Geohash.fromLatLongBits( 189 | latitude = latitudeBits - (0x1 shl (31 - bitsResolution)), 190 | longitude = longitudeBits + (0x1 shl (31 - bitsResolution)) 191 | ) 192 | 193 | fun southWest(bitsResolution: Int) = Geohash.fromLatLongBits( 194 | latitude = latitudeBits - (0x1 shl (31 - bitsResolution)), 195 | longitude = longitudeBits - (0x1 shl (31 - bitsResolution)) 196 | ) 197 | 198 | fun lower(bitsResolution: Int): Long { 199 | return bits and (0x1L.shl(64 - bitsResolution * 2).minus(1).inv()) 200 | } 201 | 202 | fun upper(bitsResolution: Int): Long { 203 | return bits or (0x1L.shl(64 - bitsResolution * 2).minus(1)) 204 | } 205 | 206 | fun range(bitsResolution: Int) = lower(bitsResolution)..upper(bitsResolution) 207 | 208 | infix fun distanceToKm(other: Geohash): Double { 209 | val latDistance = (other.latitude - this.latitude) * (PI / 180) 210 | val lonDistance = (other.longitude - this.longitude) * (PI / 180) 211 | val a = sin(latDistance / 2) * sin(latDistance / 2) + (cos(this.latitude * (PI / 180)) * cos(other.latitude * (PI / 180)) 212 | * sin(lonDistance / 2) * sin(lonDistance / 2)) 213 | val c = 2 * atan2(sqrt(a), sqrt(1 - a)) 214 | return World.radius * c 215 | } 216 | } 217 | 218 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/lightningkite/lokalize/location/GeohashCoverage.kt: -------------------------------------------------------------------------------- 1 | package com.lightningkite.lokalize.location 2 | 3 | import kotlin.math.* 4 | 5 | class GeohashCoverage( 6 | val ranges: List, 7 | val ratio: Double 8 | ) { 9 | fun simplify(): GeohashCoverage { 10 | return GeohashCoverage(simplify(ranges), ratio) 11 | } 12 | 13 | companion object { 14 | 15 | private fun simplify(ranges: List): List { 16 | val sorted = ranges.sortedBy { it.start } 17 | val out = ArrayList() 18 | 19 | var currentStart = sorted.first().start 20 | var currentEndInclusive = sorted.first().endInclusive 21 | 22 | for (item in sorted) { 23 | if (item.start > currentEndInclusive + 1) { 24 | //Out of bounds 25 | out.add(currentStart..currentEndInclusive) 26 | //Start from this range 27 | currentStart = item.start 28 | currentEndInclusive = item.endInclusive 29 | } else if (item.endInclusive > currentEndInclusive) { 30 | currentEndInclusive = item.endInclusive 31 | } 32 | } 33 | out.add(currentStart..currentEndInclusive) 34 | return out 35 | } 36 | 37 | fun countHashes( 38 | lowerLatitude: Double, 39 | upperLatitude: Double, 40 | lowerLongitude: Double, 41 | upperLongitude: Double, 42 | bits: Int 43 | ): Int { 44 | val latDegreesPerHash = 180.0 / 2.0.pow(bits) 45 | val lonDegreesPerHash = 360.0 / 2.0.pow(bits) 46 | val height = ceil((upperLatitude - lowerLatitude) / latDegreesPerHash) + 1 47 | val width = ceil((upperLongitude - lowerLongitude) / lonDegreesPerHash) + 1 48 | return (width * height).toInt() 49 | } 50 | 51 | fun coverageRatio( 52 | lowerLatitude: Double, 53 | upperLatitude: Double, 54 | lowerLongitude: Double, 55 | upperLongitude: Double, 56 | bits: Int 57 | ): Double { 58 | val latDegreesPerHash = 180.0 / 2.0.pow(bits) 59 | val lonDegreesPerHash = 360.0 / 2.0.pow(bits) 60 | val height = ceil((upperLatitude - lowerLatitude) / latDegreesPerHash) + 1 61 | val width = ceil((upperLongitude - lowerLongitude) / lonDegreesPerHash) + 1 62 | val hashCount = width * height 63 | 64 | val areaDegrees = (upperLongitude - lowerLongitude) * (upperLatitude - lowerLatitude) 65 | val coverageAreaDegrees = hashCount * latDegreesPerHash * lonDegreesPerHash 66 | return coverageAreaDegrees / areaDegrees 67 | } 68 | 69 | interface WrappingProgression : Sequence { 70 | val count: Int 71 | } 72 | 73 | val IntProgression.count: Int 74 | get() { 75 | return if ((last - first) % step == 0) 76 | (last - first) / step 77 | else 78 | (last - first) / step + 1 79 | } 80 | 81 | fun wrappingProgression(from: Int, to: Int, step: Int): WrappingProgression { 82 | return if (from > to) { 83 | val partA = (from..Int.MAX_VALUE step step) 84 | val partB = (Int.MIN_VALUE..to step step) 85 | object : WrappingProgression, Sequence by (partA.asSequence() + partB.asSequence()) { 86 | override val count: Int get() = partA.count + partB.count 87 | override fun toString(): String = "($partA) + ($partB)" 88 | } 89 | } else { 90 | val part = (from..to step step) 91 | object : WrappingProgression, Sequence by part.asSequence() { 92 | override val count: Int get() = part.count 93 | override fun toString(): String = "($part)" 94 | } 95 | 96 | } 97 | } 98 | 99 | fun cover( 100 | lowerLatitude: Double, 101 | upperLatitude: Double, 102 | lowerLongitude: Double, 103 | upperLongitude: Double, 104 | bits: Int 105 | ): GeohashCoverage { 106 | val latDegreesPerHash = 180.0 / 2.0.pow(bits) 107 | val lonDegreesPerHash = 360.0 / 2.0.pow(bits) 108 | val bitJump = 1 shl (31 - bits) 109 | val lowerGeohash = Geohash.fromLatLng(lowerLatitude, lowerLongitude) 110 | val upperGeohash = Geohash.fromLatLng(upperLatitude, upperLongitude) 111 | 112 | val latBitRange = wrappingProgression(lowerGeohash.latitudeBits, upperGeohash.latitudeBits, bitJump) 113 | val lonBitRange = wrappingProgression(lowerGeohash.longitudeBits, upperGeohash.longitudeBits, bitJump) 114 | val ranges = ArrayList() 115 | for (latBits in latBitRange) { 116 | for (lonBits in lonBitRange) { 117 | val hash = Geohash.fromLatLongBits(latBits, lonBits) 118 | ranges.add(hash.range(bits)) 119 | } 120 | } 121 | if (ranges.isEmpty()) { 122 | throw IllegalStateException("No ranges, bit ranges ${latBitRange} and ${lonBitRange}, step ${bitJump}; inputs ${lowerLatitude..upperLatitude} and ${lowerLongitude..upperLongitude}") 123 | } 124 | 125 | val simplifiedRanges = simplify(ranges) 126 | val areaDegrees = (upperLongitude - lowerLongitude) * (upperLatitude - lowerLatitude) 127 | val coverageAreaDegrees = simplifiedRanges.size * latDegreesPerHash * lonDegreesPerHash 128 | val ratio = coverageAreaDegrees / areaDegrees 129 | 130 | return GeohashCoverage(simplifiedRanges, ratio) 131 | } 132 | 133 | fun coverMaxHashes( 134 | lowerLatitude: Double, 135 | upperLatitude: Double, 136 | lowerLongitude: Double, 137 | upperLongitude: Double, 138 | hashCap: Int = 12 139 | ): GeohashCoverage { 140 | val bits = (2..32).first { countHashes(lowerLatitude, upperLatitude, lowerLongitude, upperLongitude, it) > hashCap } - 1 141 | return cover(lowerLatitude, upperLatitude, lowerLongitude, upperLongitude, bits) 142 | } 143 | 144 | fun coverRatio( 145 | lowerLatitude: Double, 146 | upperLatitude: Double, 147 | lowerLongitude: Double, 148 | upperLongitude: Double, 149 | ratioBelow: Double = 4.0 150 | ): GeohashCoverage { 151 | val bits = (2..32).first { coverageRatio(lowerLatitude, upperLatitude, lowerLongitude, upperLongitude, it) < ratioBelow } 152 | return cover(lowerLatitude, upperLatitude, lowerLongitude, upperLongitude, bits) 153 | } 154 | 155 | fun coverRadius( 156 | center: Geohash, 157 | radiusKm: Double, 158 | bits: Int 159 | ): GeohashCoverage = cover( 160 | lowerLatitude = center.latitude - radiusKm / World.latitudeDegreeKm, 161 | upperLatitude = center.latitude + radiusKm / World.latitudeDegreeKm, 162 | lowerLongitude = center.longitude - radiusKm / World.longitudeDegreeKm(center.latitude), 163 | upperLongitude = center.longitude + radiusKm / World.longitudeDegreeKm(center.latitude), 164 | bits = bits 165 | ) 166 | 167 | fun coverRadiusRatio( 168 | center: Geohash, 169 | radiusKm: Double, 170 | ratioBelow: Double = 4.0 171 | ): GeohashCoverage = coverRatio( 172 | lowerLatitude = center.latitude - radiusKm / World.latitudeDegreeKm, 173 | upperLatitude = center.latitude + radiusKm / World.latitudeDegreeKm, 174 | lowerLongitude = center.longitude - radiusKm / World.longitudeDegreeKm(center.latitude), 175 | upperLongitude = center.longitude + radiusKm / World.longitudeDegreeKm(center.latitude), 176 | ratioBelow = ratioBelow 177 | ) 178 | 179 | fun coverRadiusMaxHashes( 180 | center: Geohash, 181 | radiusKm: Double, 182 | hashCap: Int = 12 183 | ): GeohashCoverage = coverMaxHashes( 184 | lowerLatitude = center.latitude - radiusKm / World.latitudeDegreeKm, 185 | upperLatitude = center.latitude + radiusKm / World.latitudeDegreeKm, 186 | lowerLongitude = center.longitude - radiusKm / World.longitudeDegreeKm(center.latitude), 187 | upperLongitude = center.longitude + radiusKm / World.longitudeDegreeKm(center.latitude), 188 | hashCap = hashCap 189 | ) 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/lightningkite/lokalize/location/World.kt: -------------------------------------------------------------------------------- 1 | package com.lightningkite.lokalize.location 2 | 3 | import kotlin.math.PI 4 | import kotlin.math.cos 5 | 6 | object World { 7 | const val radius = 6371.0 // Radius of the earth 8 | const val latitudeDegreeKm = 110.574 9 | fun longitudeDegreeKm(latitude: Double) = 111.320 * cos(latitude * 180.0 / PI) 10 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/lightningkite/lokalize/time/Date.kt: -------------------------------------------------------------------------------- 1 | package com.lightningkite.lokalize.time 2 | 3 | import com.lightningkite.lokalize.Locale 4 | 5 | inline class Date(val daysSinceEpoch: Int) : Comparable { 6 | 7 | constructor( 8 | year: Year, 9 | month: Month, 10 | day: Int 11 | ) : this( 12 | -TimeConstants.EPOCH_STARTED_ON_DAY_AD + 13 | 1 + //Leap day in year zero 14 | year.sinceAD * TimeConstants.DAYS_PER_YEAR + 15 | year.sinceAD / 4 - 16 | year.sinceAD / 100 + 17 | year.sinceAD / 400 + 18 | (if (year.isLeap) -1 else 0) + 19 | (if (year.isLeap) month.startDayInLeapYear else month.startDayInNormalYear) + 20 | (day - 1) //Need to get day back to being zero-indexed 21 | ) 22 | 23 | override fun compareTo(other: Date): Int = daysSinceEpoch.compareTo(other.daysSinceEpoch) 24 | 25 | companion object { 26 | fun iso8601(string: String): Date = Date( 27 | year = Year(string.substringBefore('-').toIntOrNull() ?: 1970), 28 | month = Month.values()[string.substringAfter('-').substringBefore('-').toIntOrNull()?.minus(1) ?: 0], 29 | day = string.substringAfterLast('-').toIntOrNull() ?: 0 30 | ) 31 | 32 | val MIN = Date(Int.MIN_VALUE) 33 | val MAX = Date(Int.MAX_VALUE) 34 | 35 | } 36 | 37 | private val yearAndDayInYear: YearAndDayInYear 38 | get() { 39 | var remainingDays = daysSinceEpoch + TimeConstants.EPOCH_STARTED_ON_DAY_AD - 1 40 | 41 | val setsOf400Years = remainingDays / TimeConstants.DAYS_PER_400_YEARS 42 | remainingDays -= setsOf400Years * TimeConstants.DAYS_PER_400_YEARS 43 | val setsOf100Years = remainingDays / TimeConstants.DAYS_PER_100_YEARS 44 | remainingDays -= setsOf100Years * TimeConstants.DAYS_PER_100_YEARS 45 | val setsOf4Years = remainingDays / TimeConstants.DAYS_PER_4_YEARS 46 | remainingDays -= setsOf4Years * TimeConstants.DAYS_PER_4_YEARS 47 | val extraYears = remainingDays / TimeConstants.DAYS_PER_YEAR 48 | remainingDays -= extraYears * TimeConstants.DAYS_PER_YEAR 49 | 50 | val year = Year(setsOf400Years * 400 + setsOf100Years * 100 + setsOf4Years * 4 + extraYears) 51 | 52 | val daysIntoYear = if (year.isLeap && extraYears != 4 && setsOf4Years != 25 && setsOf100Years != 4) remainingDays + 1 else remainingDays 53 | return YearAndDayInYear.make(year, daysIntoYear) 54 | } 55 | 56 | @Suppress("NOTHING_TO_INLINE") 57 | private inline infix fun Int.remPositive(other: Int): Int = this.rem(other).plus(other).rem(other) 58 | 59 | val dayOfWeek: DayOfWeek get() = DayOfWeek.values()[(daysSinceEpoch + 4) remPositive 7] 60 | fun toNextDayOfWeek(value: DayOfWeek): Date { 61 | val current = dayOfWeek 62 | return Date(daysSinceEpoch + ((value.ordinal + 7 - current.ordinal) remPositive 7)) 63 | } 64 | 65 | fun toDayInSameWeek(value: DayOfWeek): Date { 66 | val current = dayOfWeek 67 | return Date(daysSinceEpoch + value.ordinal - current.ordinal) 68 | } 69 | 70 | val dayOfYear: Int get() = yearAndDayInYear.dayInYear 71 | fun toDayInSameYear(value: Int): Date { 72 | val current = dayOfYear 73 | return Date(daysSinceEpoch + value - current) 74 | } 75 | 76 | val dayOfMonth: Int get() = yearAndDayInYear.dayOfMonth 77 | 78 | fun toDayInMonth(value: Int): Date { 79 | val current = dayOfMonth 80 | return Date(daysSinceEpoch + value - current) 81 | } 82 | 83 | val month: Month get() = yearAndDayInYear.month 84 | 85 | fun toMonthInYear(value: Month): Date { 86 | val year = year 87 | return Date(year, value, dayOfMonth.coerceAtMost(if (year.isLeap) value.daysLeap else value.days)) 88 | } 89 | 90 | val year: Year get() = yearAndDayInYear.year 91 | 92 | fun toYear(year: Year) = Date(year, month, dayOfMonth) 93 | 94 | fun iso8601(): String = "${year.sinceAD.toString().padStart(4, '0')}-${month.ordinal.plus(1).toString().padStart(2, '0')}-${dayOfMonth.toString().padStart(2, '0')}" 95 | 96 | operator fun minus(other: Date) = Duration((daysSinceEpoch - other.daysSinceEpoch).times(TimeConstants.MS_PER_DAY)) 97 | 98 | override fun toString(): String = Locale.default.renderDate(this) 99 | } 100 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/lightningkite/lokalize/time/DateTime.kt: -------------------------------------------------------------------------------- 1 | package com.lightningkite.lokalize.time 2 | 3 | import com.lightningkite.lokalize.DefaultLocale 4 | import com.lightningkite.lokalize.Locale 5 | 6 | data class DateTime(val date: Date, val time: Time) : Comparable { 7 | 8 | override fun compareTo(other: DateTime): Int { 9 | val dateResult = date.compareTo(other.date) 10 | if (dateResult != 0) return dateResult 11 | return time.compareTo(other.time) 12 | } 13 | 14 | companion object { 15 | fun iso8601(string: String): DateTime { 16 | return DateTime( 17 | date = Date.iso8601(string.substringBefore('T')), 18 | time = Time.iso8601(string.substringAfterLast('T').substringBefore('+')) 19 | ) 20 | } 21 | } 22 | 23 | fun toTimeStamp(offset: Duration = Duration(DefaultLocale.getTimeOffsetMilliseconds())) = TimeStamp(date, time, offset) 24 | 25 | fun iso8601(offset: Duration = Duration(DefaultLocale.getTimeOffsetMilliseconds())):String = date.iso8601() + "T" + time.iso8601() + "+" + offset.hours.toString().padStart(2, '0') + ":" + offset.minutes.toString().padStart(2, '0') 26 | 27 | operator fun minus(other: DateTime) = (date - other.date) + (time - other.time) 28 | override fun toString(): String = Locale.default.renderDateTime(this) 29 | } 30 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/lightningkite/lokalize/time/DayOfWeek.kt: -------------------------------------------------------------------------------- 1 | package com.lightningkite.lokalize.time 2 | 3 | enum class DayOfWeek { 4 | Sunday, 5 | Monday, 6 | Tuesday, 7 | Wednesday, 8 | Thursday, 9 | Friday, 10 | Saturday 11 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/lightningkite/lokalize/time/DaysOfWeek.kt: -------------------------------------------------------------------------------- 1 | package com.lightningkite.lokalize.time 2 | 3 | class DaysOfWeek( 4 | var sunday: Boolean = false, 5 | var monday: Boolean = false, 6 | var tuesday: Boolean = false, 7 | var wednesday: Boolean = false, 8 | var thursday: Boolean = false, 9 | var friday: Boolean = false, 10 | var saturday: Boolean = false 11 | ) { 12 | val setView: MutableSet get() { 13 | return object : MutableSet { 14 | override val size: Int get() = iterator().asSequence().count() 15 | 16 | override fun add(element: DayOfWeek): Boolean { 17 | return if(!get(element)){ 18 | set(element, true) 19 | true 20 | } else { 21 | false 22 | } 23 | } 24 | 25 | override fun addAll(elements: Collection): Boolean = elements.any { add(it) } 26 | 27 | override fun clear() { 28 | sunday = false 29 | monday = false 30 | tuesday = false 31 | wednesday = false 32 | thursday = false 33 | friday = false 34 | saturday = false 35 | } 36 | 37 | override fun contains(element: DayOfWeek): Boolean = get(element) 38 | 39 | override fun containsAll(elements: Collection): Boolean = elements.all { get(it) } 40 | 41 | override fun isEmpty(): Boolean = 42 | !sunday && 43 | !monday && 44 | !tuesday && 45 | !wednesday && 46 | !thursday && 47 | !friday && 48 | !saturday 49 | 50 | override fun iterator(): MutableIterator = object : MutableIterator { 51 | var index = -1 52 | override fun hasNext(): Boolean { 53 | while(index < 7 && !get(DayOfWeek.values()[index])){ 54 | index++ 55 | } 56 | return index != 7 57 | } 58 | 59 | override fun next(): DayOfWeek { 60 | while(index < 7 && !get(DayOfWeek.values()[index])){ 61 | index++ 62 | } 63 | if(index == 7) throw IllegalStateException("Iterator used incorrectly") 64 | return DayOfWeek.values()[index] 65 | } 66 | 67 | override fun remove() { 68 | set(DayOfWeek.values()[index], false) 69 | } 70 | } 71 | 72 | override fun remove(element: DayOfWeek): Boolean { 73 | return if(get(element)){ 74 | set(element, false) 75 | true 76 | } else { 77 | false 78 | } 79 | } 80 | 81 | override fun removeAll(elements: Collection): Boolean = elements.any { remove(it) } 82 | 83 | override fun retainAll(elements: Collection): Boolean { 84 | var changed = false 85 | DayOfWeek.values().forEach { 86 | if(!elements.contains(it)){ 87 | set(it, false) 88 | changed = true 89 | } 90 | } 91 | return changed 92 | } 93 | } 94 | } 95 | 96 | companion object { 97 | fun trueByDefault( 98 | sunday: Boolean = false, 99 | monday: Boolean = false, 100 | tuesday: Boolean = false, 101 | wednesday: Boolean = false, 102 | thursday: Boolean = false, 103 | friday: Boolean = false, 104 | saturday: Boolean = false 105 | ): DaysOfWeek = DaysOfWeek( 106 | sunday = sunday, 107 | monday = monday, 108 | tuesday = tuesday, 109 | wednesday = wednesday, 110 | thursday = thursday, 111 | friday = friday, 112 | saturday = saturday 113 | ) 114 | } 115 | 116 | operator fun get(dayOfWeek: DayOfWeek): Boolean { 117 | return when (dayOfWeek) { 118 | DayOfWeek.Sunday -> sunday 119 | DayOfWeek.Monday -> monday 120 | DayOfWeek.Tuesday -> tuesday 121 | DayOfWeek.Wednesday -> wednesday 122 | DayOfWeek.Thursday -> thursday 123 | DayOfWeek.Friday -> friday 124 | DayOfWeek.Saturday -> saturday 125 | } 126 | } 127 | 128 | operator fun set(dayOfWeek: DayOfWeek, value: Boolean) { 129 | when (dayOfWeek) { 130 | DayOfWeek.Sunday -> sunday = value 131 | DayOfWeek.Monday -> monday = value 132 | DayOfWeek.Tuesday -> tuesday = value 133 | DayOfWeek.Wednesday -> wednesday = value 134 | DayOfWeek.Thursday -> thursday = value 135 | DayOfWeek.Friday -> friday = value 136 | DayOfWeek.Saturday -> saturday = value 137 | } 138 | } 139 | 140 | val ranges: List> get(){ 141 | val output = ArrayList>() 142 | var lastStart: DayOfWeek? = null 143 | var previous: DayOfWeek? = null 144 | DayOfWeek.values().forEach { 145 | if (get(it)) { 146 | if (lastStart == null) { 147 | lastStart = it 148 | } 149 | } else { 150 | if (lastStart != null && previous != null) { 151 | output.add(lastStart!!..previous!!) 152 | } 153 | lastStart = null 154 | } 155 | previous = it 156 | } 157 | return output 158 | } 159 | 160 | override fun toString(): String = ranges.joinToString { 161 | if (it.start == it.endInclusive) 162 | it.start.name 163 | else 164 | it.start.name + " - " + it.endInclusive.name 165 | } 166 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/lightningkite/lokalize/time/Duration.kt: -------------------------------------------------------------------------------- 1 | package com.lightningkite.lokalize.time 2 | 3 | import kotlin.math.abs 4 | 5 | inline class Duration(val milliseconds: Long) : Comparable { 6 | 7 | override fun compareTo(other: Duration): Int = milliseconds.compareTo(other.milliseconds) 8 | 9 | companion object { 10 | fun milliseconds(milliseconds: Long) = Duration(milliseconds) 11 | 12 | fun seconds(seconds: Long) = Duration(seconds * TimeConstants.MS_PER_SECOND) 13 | fun minutes(minutes: Long) = Duration(minutes * TimeConstants.MS_PER_MINUTE) 14 | fun hours(hours: Long) = Duration(hours * TimeConstants.MS_PER_HOUR) 15 | fun days(days: Long) = Duration(days * TimeConstants.MS_PER_DAY) 16 | 17 | fun seconds(seconds: Float) = Duration((seconds * TimeConstants.MS_PER_SECOND).toLong()) 18 | fun minutes(minutes: Float) = Duration((minutes * TimeConstants.MS_PER_MINUTE).toLong()) 19 | fun hours(hours: Float) = Duration((hours * TimeConstants.MS_PER_HOUR).toLong()) 20 | fun days(days: Float) = Duration((days * TimeConstants.MS_PER_DAY).toLong()) 21 | 22 | fun seconds(seconds: Double) = Duration((seconds * TimeConstants.MS_PER_SECOND).toLong()) 23 | fun minutes(minutes: Double) = Duration((minutes * TimeConstants.MS_PER_MINUTE).toLong()) 24 | fun hours(hours: Double) = Duration((hours * TimeConstants.MS_PER_HOUR).toLong()) 25 | fun days(days: Double) = Duration((days * TimeConstants.MS_PER_DAY).toLong()) 26 | 27 | val zero = Duration(0) 28 | } 29 | 30 | val seconds: Long get() = milliseconds / TimeConstants.MS_PER_SECOND 31 | val minutes: Long get() = milliseconds / TimeConstants.MS_PER_MINUTE 32 | val hours: Long get() = milliseconds / TimeConstants.MS_PER_HOUR 33 | val days: Long get() = milliseconds / TimeConstants.MS_PER_DAY 34 | val weeks: Long get() = milliseconds / TimeConstants.MS_PER_WEEK 35 | val years: Long get() = milliseconds / TimeConstants.MS_PER_YEAR 36 | 37 | val secondsDouble: Double get() = milliseconds.toDouble() / TimeConstants.MS_PER_SECOND 38 | val minutesDouble: Double get() = milliseconds.toDouble() / TimeConstants.MS_PER_MINUTE 39 | val hoursDouble: Double get() = milliseconds.toDouble() / TimeConstants.MS_PER_HOUR 40 | val daysDouble: Double get() = milliseconds.toDouble() / TimeConstants.MS_PER_DAY 41 | val weeksDouble: Double get() = milliseconds.toDouble() / TimeConstants.MS_PER_WEEK 42 | val yearsDouble: Double get() = milliseconds.toDouble() / TimeConstants.MS_PER_YEAR 43 | 44 | operator fun plus(other: ShortDuration) = Duration(milliseconds + other.milliseconds) 45 | operator fun minus(other: ShortDuration) = Duration(milliseconds - other.milliseconds) 46 | operator fun plus(other: Duration) = Duration(milliseconds + other.milliseconds) 47 | operator fun minus(other: Duration) = Duration(milliseconds - other.milliseconds) 48 | operator fun times(scale: Int) = Duration((milliseconds * scale)) 49 | operator fun times(scale: Float) = Duration((milliseconds * scale).toLong()) 50 | operator fun times(scale: Double) = Duration((milliseconds * scale).toLong()) 51 | operator fun div(scale: Int) = Duration((milliseconds / scale)) 52 | operator fun div(scale: Float) = Duration((milliseconds / scale).toLong()) 53 | operator fun div(scale: Double) = Duration((milliseconds / scale).toLong()) 54 | 55 | fun toShortDuration(): ShortDuration = ShortDuration.milliseconds(milliseconds) 56 | 57 | fun bySignificantUnit(): Pair { 58 | val absolute = abs(milliseconds) 59 | for(unit in TimeUnit.values().reversed()) { 60 | if(absolute > unit.milliseconds){ 61 | return unit to (milliseconds.toDouble() / unit.milliseconds) 62 | } 63 | } 64 | return TimeUnit.Milliseconds to milliseconds.toDouble() 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/lightningkite/lokalize/time/Month.kt: -------------------------------------------------------------------------------- 1 | package com.lightningkite.lokalize.time 2 | 3 | enum class Month(val days: Int, val daysLeap: Int = days) { 4 | January(31), 5 | February(28, 29), 6 | March(31), 7 | April(30), 8 | May(31), 9 | June(30), 10 | July(31), 11 | August(31), 12 | September(30), 13 | October(31), 14 | November(30), 15 | December(31); 16 | 17 | companion object { 18 | val monthStartInLeapYear = run { 19 | var dayCounter = 0 20 | IntArray(12) { index -> 21 | val result = dayCounter 22 | dayCounter += Month.values()[index].daysLeap 23 | result 24 | } 25 | } 26 | val monthStartInNormalYear = run { 27 | var dayCounter = 0 28 | IntArray(12) { index -> 29 | val result = dayCounter 30 | dayCounter += Month.values()[index].days 31 | result 32 | } 33 | } 34 | } 35 | 36 | val startDayInLeapYear: Int get() = monthStartInLeapYear[this.ordinal] 37 | val startDayInNormalYear: Int get() = monthStartInNormalYear[this.ordinal] 38 | 39 | fun days(year: Year) = if (year.isLeap) daysLeap else days 40 | } 41 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/lightningkite/lokalize/time/ShortDuration.kt: -------------------------------------------------------------------------------- 1 | package com.lightningkite.lokalize.time 2 | 3 | inline class ShortDuration(val nanoseconds: Long) : Comparable { 4 | 5 | override fun compareTo(other: ShortDuration): Int = nanoseconds.compareTo(other.nanoseconds) 6 | 7 | companion object { 8 | fun nanoseconds(nanoseconds: Long) = ShortDuration(nanoseconds) 9 | 10 | fun milliseconds(milliseconds: Long) = ShortDuration(milliseconds * TimeConstants.NS_PER_MILLISECOND) 11 | fun seconds(seconds: Long) = ShortDuration(seconds * TimeConstants.NS_PER_SECOND) 12 | fun minutes(seconds: Long) = ShortDuration(seconds * TimeConstants.NS_PER_MINUTE) 13 | fun hours(seconds: Long) = ShortDuration(seconds * TimeConstants.NS_PER_HOUR) 14 | fun days(seconds: Long) = ShortDuration(seconds * TimeConstants.NS_PER_DAY) 15 | 16 | fun seconds(seconds: Float) = ShortDuration((seconds * TimeConstants.NS_PER_SECOND).toLong()) 17 | fun minutes(seconds: Float) = ShortDuration((seconds * TimeConstants.NS_PER_MINUTE).toLong()) 18 | fun hours(seconds: Float) = ShortDuration((seconds * TimeConstants.NS_PER_HOUR).toLong()) 19 | fun days(seconds: Float) = ShortDuration((seconds * TimeConstants.NS_PER_DAY).toLong()) 20 | 21 | fun seconds(seconds: Double) = ShortDuration((seconds * TimeConstants.NS_PER_SECOND).toLong()) 22 | fun minutes(seconds: Double) = ShortDuration((seconds * TimeConstants.NS_PER_MINUTE).toLong()) 23 | fun hours(seconds: Double) = ShortDuration((seconds * TimeConstants.NS_PER_HOUR).toLong()) 24 | fun days(seconds: Double) = ShortDuration((seconds * TimeConstants.NS_PER_DAY).toLong()) 25 | 26 | inline fun measure(action: () -> Unit): ShortDuration { 27 | val start = ShortDuration.get() 28 | action() 29 | val end = ShortDuration.get() 30 | return end - start 31 | } 32 | } 33 | 34 | val milliseconds: Long get() = nanoseconds / TimeConstants.NS_PER_SECOND 35 | val seconds: Long get() = nanoseconds / TimeConstants.NS_PER_SECOND 36 | val minutes: Long get() = nanoseconds / TimeConstants.NS_PER_MINUTE 37 | val hours: Long get() = nanoseconds / TimeConstants.NS_PER_HOUR 38 | val days: Long get() = nanoseconds / TimeConstants.NS_PER_DAY 39 | 40 | val millisecondsDouble: Double get() = nanoseconds.toDouble() / TimeConstants.NS_PER_MILLISECOND 41 | val secondsDouble: Double get() = nanoseconds.toDouble() / TimeConstants.NS_PER_SECOND 42 | val minutesDouble: Double get() = nanoseconds.toDouble() / TimeConstants.NS_PER_MINUTE 43 | val hoursDouble: Double get() = nanoseconds.toDouble() / TimeConstants.NS_PER_HOUR 44 | val daysDouble: Double get() = nanoseconds.toDouble() / TimeConstants.NS_PER_DAY 45 | 46 | operator fun plus(other: ShortDuration) = ShortDuration(nanoseconds + other.nanoseconds) 47 | operator fun minus(other: ShortDuration) = ShortDuration(nanoseconds - other.nanoseconds) 48 | operator fun plus(other: Duration) = 49 | ShortDuration(nanoseconds + other.milliseconds * TimeConstants.NS_PER_MILLISECOND) 50 | 51 | operator fun minus(other: Duration) = 52 | ShortDuration(nanoseconds - other.milliseconds * TimeConstants.NS_PER_MILLISECOND) 53 | 54 | operator fun times(scale: Int) = ShortDuration((nanoseconds * scale)) 55 | operator fun times(scale: Float) = ShortDuration((nanoseconds * scale).toLong()) 56 | operator fun times(scale: Double) = ShortDuration((nanoseconds * scale).toLong()) 57 | operator fun div(scale: Int) = ShortDuration((nanoseconds / scale)) 58 | operator fun div(scale: Float) = ShortDuration((nanoseconds / scale).toLong()) 59 | operator fun div(scale: Double) = ShortDuration((nanoseconds / scale).toLong()) 60 | 61 | fun toDuration(): Duration = Duration.milliseconds(milliseconds) 62 | } 63 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/com/lightningkite/lokalize/time/Time.kt: -------------------------------------------------------------------------------- 1 | package com.lightningkite.lokalize.time 2 | 3 | import com.lightningkite.lokalize.Locale 4 | 5 | 6 | inline class Time(val millisecondsSinceMidnight: Int) : Comparable