├── Shared ├── README.md └── Times │ ├── Kuwait City-Kuwait.json │ ├── Doha-Qatar.json │ ├── Makkah-UmmAlQura.json │ ├── London-MoonsightingCommittee.json │ ├── Dubai-Gulf.json │ └── Ankara-Turkey.json ├── codecov.yml ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── samples ├── README.md ├── build.gradle.kts └── src │ └── main │ └── java │ └── com │ └── batoulapps │ └── adhan2 │ └── Example.kt ├── adhan ├── src │ ├── commonMain │ │ └── kotlin │ │ │ └── com │ │ │ └── batoulapps │ │ │ └── adhan2 │ │ │ ├── model │ │ │ ├── Rounding.kt │ │ │ └── Shafaq.kt │ │ │ ├── internal │ │ │ ├── ShadowLength.kt │ │ │ ├── DoubleExtensions.kt │ │ │ ├── DoubleUtil.kt │ │ │ ├── QiblaUtil.kt │ │ │ ├── CalendricalHelper.kt │ │ │ ├── SolarTime.kt │ │ │ ├── SolarCoordinates.kt │ │ │ └── Astronomical.kt │ │ │ ├── Prayer.kt │ │ │ ├── Qibla.kt │ │ │ ├── Coordinates.kt │ │ │ ├── PrayerAdjustments.kt │ │ │ ├── Madhab.kt │ │ │ ├── data │ │ │ ├── TimeComponents.kt │ │ │ ├── DateComponents.kt │ │ │ └── CalendarUtil.kt │ │ │ ├── HighLatitudeRule.kt │ │ │ ├── SunnahTimes.kt │ │ │ ├── CalculationParameters.kt │ │ │ ├── CalculationMethod.kt │ │ │ └── PrayerTimes.kt │ ├── commonTest │ │ └── kotlin │ │ │ └── com │ │ │ └── batoulapps │ │ │ └── adhan2 │ │ │ ├── TestUtil.kt │ │ │ ├── data │ │ │ ├── TimingFile.kt │ │ │ ├── TimingInfo.kt │ │ │ └── TimingParameters.kt │ │ │ ├── CalculationParametersTest.kt │ │ │ ├── internal │ │ │ ├── TestUtils.kt │ │ │ ├── MathTest.kt │ │ │ └── AstronomicalTest.kt │ │ │ ├── QiblaTest.kt │ │ │ ├── CalculationMethodTest.kt │ │ │ ├── TimingTest.kt │ │ │ ├── SunnahTimesTest.kt │ │ │ └── PrayerTimesTest.kt │ ├── jvmTest │ │ └── kotlin │ │ │ └── com │ │ │ └── batoulapps │ │ │ └── adhan2 │ │ │ └── TestUtil.jvm.kt │ ├── wasmJsTest │ │ └── kotlin │ │ │ └── com │ │ │ └── batoulapps │ │ │ └── adhan2 │ │ │ └── TestUtil.wasmJs.kt │ ├── nativeTest │ │ └── kotlin │ │ │ └── com │ │ │ └── batoulapps │ │ │ └── adhan2 │ │ │ └── TestUtil.native.kt │ └── jsTest │ │ └── kotlin │ │ └── com │ │ └── batoulapps │ │ └── adhan2 │ │ └── TestUtil.js.kt └── build.gradle.kts ├── settings.gradle.kts ├── .gitignore ├── kotlin-js-store ├── wasm │ └── yarn.lock └── yarn.lock ├── .github └── workflows │ └── build.yml ├── CHANGELOG.md ├── LICENSE ├── gradle.properties ├── gradlew.bat ├── README.md └── gradlew /Shared/README.md: -------------------------------------------------------------------------------- 1 | # adhan-testdata 2 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | ignore: 2 | - samples 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/batoulapps/adhan-kotlin/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /samples/README.md: -------------------------------------------------------------------------------- 1 | # Adhan Example 2 | 3 | This is a simple command line example for Adhan. You can run it by: 4 | 5 | ``` 6 | ./gradlew :samples:run 7 | ``` 8 | -------------------------------------------------------------------------------- /adhan/src/commonMain/kotlin/com/batoulapps/adhan2/model/Rounding.kt: -------------------------------------------------------------------------------- 1 | package com.batoulapps.adhan2.model 2 | 3 | enum class Rounding { 4 | NEAREST, 5 | UP, 6 | NONE 7 | } -------------------------------------------------------------------------------- /adhan/src/commonMain/kotlin/com/batoulapps/adhan2/internal/ShadowLength.kt: -------------------------------------------------------------------------------- 1 | package com.batoulapps.adhan2.internal 2 | 3 | enum class ShadowLength(val shadowLength: Double) { 4 | SINGLE(1.0), 5 | DOUBLE(2.0); 6 | } -------------------------------------------------------------------------------- /adhan/src/commonMain/kotlin/com/batoulapps/adhan2/Prayer.kt: -------------------------------------------------------------------------------- 1 | package com.batoulapps.adhan2 2 | 3 | enum class Prayer { 4 | NONE, 5 | FAJR, 6 | SUNRISE, 7 | DHUHR, 8 | ASR, 9 | MAGHRIB, 10 | ISHA 11 | } -------------------------------------------------------------------------------- /adhan/src/commonMain/kotlin/com/batoulapps/adhan2/internal/DoubleExtensions.kt: -------------------------------------------------------------------------------- 1 | package com.batoulapps.adhan2.internal 2 | 3 | import kotlin.math.PI 4 | 5 | fun Double.toRadians() = this * PI / 180 6 | fun Double.toDegrees() = (this * 180) / PI 7 | -------------------------------------------------------------------------------- /adhan/src/commonTest/kotlin/com/batoulapps/adhan2/TestUtil.kt: -------------------------------------------------------------------------------- 1 | package com.batoulapps.adhan2 2 | 3 | import okio.FileSystem 4 | 5 | expect class TestUtil() { 6 | fun fileSystem(): FileSystem? 7 | fun environmentVariable(name: String): String? 8 | } -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | mavenCentral() 5 | maven { url = uri("https://plugins.gradle.org/m2/") } 6 | } 7 | 8 | } 9 | rootProject.name = "Adhan" 10 | include(":adhan") 11 | include(":samples") 12 | -------------------------------------------------------------------------------- /adhan/src/commonTest/kotlin/com/batoulapps/adhan2/data/TimingFile.kt: -------------------------------------------------------------------------------- 1 | package com.batoulapps.adhan2.data 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class TimingFile( 7 | val params: TimingParameters, 8 | val times: List, 9 | val variance: Long = 0 10 | ) -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /adhan/src/jvmTest/kotlin/com/batoulapps/adhan2/TestUtil.jvm.kt: -------------------------------------------------------------------------------- 1 | package com.batoulapps.adhan2 2 | 3 | import okio.FileSystem 4 | 5 | actual class TestUtil actual constructor() { 6 | actual fun fileSystem(): FileSystem? = FileSystem.SYSTEM 7 | actual fun environmentVariable(name: String): String? = System.getenv(name) 8 | } -------------------------------------------------------------------------------- /adhan/src/commonMain/kotlin/com/batoulapps/adhan2/Qibla.kt: -------------------------------------------------------------------------------- 1 | package com.batoulapps.adhan2 2 | 3 | import com.batoulapps.adhan2.internal.QiblaUtil 4 | 5 | class Qibla(coordinates: Coordinates) { 6 | val direction: Double = QiblaUtil.calculateQiblaDirection(coordinates) 7 | 8 | companion object { 9 | private val MAKKAH = Coordinates(21.4225241, 39.8261818) 10 | } 11 | } -------------------------------------------------------------------------------- /adhan/src/commonTest/kotlin/com/batoulapps/adhan2/data/TimingInfo.kt: -------------------------------------------------------------------------------- 1 | package com.batoulapps.adhan2.data 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class TimingInfo( 7 | val date: String, 8 | val fajr: String, 9 | val sunrise: String, 10 | val dhuhr: String, 11 | val asr: String, 12 | val maghrib: String, 13 | val isha: String 14 | ) -------------------------------------------------------------------------------- /adhan/src/commonTest/kotlin/com/batoulapps/adhan2/data/TimingParameters.kt: -------------------------------------------------------------------------------- 1 | package com.batoulapps.adhan2.data 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class TimingParameters( 7 | val latitude: Double = 0.0, 8 | val longitude: Double = 0.0, 9 | val timezone: String, 10 | val method: String, 11 | val madhab: String, 12 | val highLatitudeRule: String 13 | ) -------------------------------------------------------------------------------- /adhan/src/wasmJsTest/kotlin/com/batoulapps/adhan2/TestUtil.wasmJs.kt: -------------------------------------------------------------------------------- 1 | package com.batoulapps.adhan2 2 | 3 | import okio.FileSystem 4 | 5 | @JsModule("@js-joda/timezone") 6 | external object JsJodaTimeZoneModule 7 | 8 | private val jsJodaTz = JsJodaTimeZoneModule 9 | 10 | actual class TestUtil actual constructor() { 11 | actual fun fileSystem(): FileSystem? = null 12 | actual fun environmentVariable(name: String): String? = null 13 | } -------------------------------------------------------------------------------- /samples/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 2 | 3 | plugins { 4 | kotlin("jvm") 5 | application 6 | } 7 | 8 | repositories { 9 | mavenCentral() 10 | } 11 | 12 | dependencies { 13 | implementation(project(":adhan")) 14 | } 15 | 16 | kotlin { 17 | compilerOptions { 18 | optIn.add("kotlin.time.ExperimentalTime") 19 | } 20 | } 21 | 22 | application { 23 | mainClass.set("com.batoulapps.adhan2.Example") 24 | } 25 | -------------------------------------------------------------------------------- /adhan/src/commonMain/kotlin/com/batoulapps/adhan2/Coordinates.kt: -------------------------------------------------------------------------------- 1 | package com.batoulapps.adhan2 2 | 3 | /** 4 | * Coordinates representing a particular place 5 | */ 6 | class Coordinates( 7 | val latitude: Double, 8 | val longitude: Double 9 | ) { 10 | 11 | init { 12 | require(latitude in -90.0..90.0) { "Latitude must be between -90 and 90 degrees" } 13 | require(longitude in -180.0..180.0) { "Longitude must be between -180 and 180 degrees" } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /adhan/src/commonMain/kotlin/com/batoulapps/adhan2/PrayerAdjustments.kt: -------------------------------------------------------------------------------- 1 | package com.batoulapps.adhan2 2 | 3 | /** 4 | * Adjustment value for prayer times, in minutes 5 | * These values are added (or subtracted) from the prayer time that is calculated before 6 | * returning the result times. 7 | */ 8 | data class PrayerAdjustments( 9 | val fajr: Int = 0, 10 | val sunrise: Int = 0, 11 | val dhuhr: Int = 0, 12 | val asr: Int = 0, 13 | val maghrib: Int = 0, 14 | val isha: Int = 0 15 | ) -------------------------------------------------------------------------------- /adhan/src/nativeTest/kotlin/com/batoulapps/adhan2/TestUtil.native.kt: -------------------------------------------------------------------------------- 1 | package com.batoulapps.adhan2 2 | 3 | import kotlinx.cinterop.ExperimentalForeignApi 4 | import kotlinx.cinterop.toKString 5 | import okio.FileSystem 6 | import platform.posix.getenv 7 | 8 | @OptIn(ExperimentalForeignApi::class) 9 | actual class TestUtil actual constructor() { 10 | actual fun fileSystem(): FileSystem? = FileSystem.SYSTEM 11 | actual fun environmentVariable(name: String): String? = getenv(name)?.toKString() 12 | } -------------------------------------------------------------------------------- /adhan/src/jsTest/kotlin/com/batoulapps/adhan2/TestUtil.js.kt: -------------------------------------------------------------------------------- 1 | package com.batoulapps.adhan2 2 | 3 | import okio.FileSystem 4 | import okio.NodeJsFileSystem 5 | 6 | @JsModule("@js-joda/timezone") 7 | @JsNonModule 8 | external object JsJodaTimeZoneModule 9 | 10 | private val jsJodaTz = JsJodaTimeZoneModule 11 | 12 | actual class TestUtil actual constructor() { 13 | actual fun fileSystem(): FileSystem? = NodeJsFileSystem 14 | actual fun environmentVariable(name: String): String? = js("globalThis.process.env[name]") as String? 15 | } -------------------------------------------------------------------------------- /adhan/src/commonMain/kotlin/com/batoulapps/adhan2/Madhab.kt: -------------------------------------------------------------------------------- 1 | package com.batoulapps.adhan2 2 | 3 | import com.batoulapps.adhan2.internal.ShadowLength 4 | import com.batoulapps.adhan2.internal.ShadowLength.DOUBLE 5 | import com.batoulapps.adhan2.internal.ShadowLength.SINGLE 6 | 7 | /** 8 | * Madhab for determining how Asr is calculated 9 | */ 10 | enum class Madhab { 11 | /** 12 | * Shafi Madhab 13 | */ 14 | SHAFI, 15 | 16 | /** 17 | * Hanafi Madhab 18 | */ 19 | HANAFI; 20 | 21 | val shadowLength: ShadowLength 22 | get() = when (this) { 23 | SHAFI -> SINGLE 24 | HANAFI -> DOUBLE 25 | } 26 | } -------------------------------------------------------------------------------- /adhan/src/commonMain/kotlin/com/batoulapps/adhan2/internal/DoubleUtil.kt: -------------------------------------------------------------------------------- 1 | package com.batoulapps.adhan2.internal 2 | 3 | import kotlin.math.floor 4 | import kotlin.math.roundToInt 5 | 6 | internal object DoubleUtil { 7 | fun normalizeWithBound(value: Double, max: Double): Double { 8 | return value - max * floor(value / max) 9 | } 10 | 11 | fun unwindAngle(value: Double): Double { 12 | return normalizeWithBound(value, 360.0) 13 | } 14 | 15 | fun closestAngle(angle: Double): Double { 16 | return if (angle >= -180 && angle <= 180) { 17 | angle 18 | } else angle - 360 * (angle / 360).roundToInt() 19 | } 20 | } -------------------------------------------------------------------------------- /adhan/src/commonMain/kotlin/com/batoulapps/adhan2/model/Shafaq.kt: -------------------------------------------------------------------------------- 1 | package com.batoulapps.adhan2.model 2 | 3 | /** 4 | * Shafaq is the twilight in the sky. Different madhabs define the appearance of 5 | * twilight differently. These values are used by the MoonsightingComittee method 6 | * for the different ways to calculate Isha. 7 | */ 8 | enum class Shafaq { 9 | // General is a combination of Ahmer and Abyad. 10 | GENERAL, 11 | 12 | // Ahmar means the twilight is the red glow in the sky. Used by the Shafi, Maliki, and Hanbali madhabs. 13 | AHMER, 14 | 15 | // Abyad means the twilight is the white glow in the sky. Used by the Hanafi madhab. 16 | ABYAD 17 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # java 2 | *.class 3 | 4 | # intellij 5 | *.iml 6 | .idea/ 7 | 8 | # VSCode 9 | .project 10 | .settings 11 | .classpath 12 | 13 | # fastlane 14 | # 15 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 16 | # screenshots whenever they are needed. 17 | # For more information about the recommended setup visit: 18 | # https://github.com/fastlane/fastlane/blob/master/docs/Gitignore.md 19 | 20 | fastlane/report.xml 21 | fastlane/screenshots 22 | 23 | # target directories 24 | adhan/target 25 | samples/target 26 | 27 | # build 28 | build/ 29 | 30 | # gradle 31 | .gradle 32 | local.properties 33 | 34 | # kotlin 35 | .kotlin 36 | -------------------------------------------------------------------------------- /kotlin-js-store/wasm/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@js-joda/core@3.2.0": 6 | version "3.2.0" 7 | resolved "https://registry.yarnpkg.com/@js-joda/core/-/core-3.2.0.tgz#3e61e21b7b2b8a6be746df1335cf91d70db2a273" 8 | integrity sha512-PMqgJ0sw5B7FKb2d5bWYIoxjri+QlW/Pys7+Rw82jSH0QN3rB05jZ/VrrsUdh1w4+i2kw9JOejXGq/KhDOX7Kg== 9 | 10 | "@js-joda/timezone@2.21.1": 11 | version "2.21.1" 12 | resolved "https://registry.yarnpkg.com/@js-joda/timezone/-/timezone-2.21.1.tgz#c552dbbf5b58a4e861dfb7b43caa2cc3bae852b0" 13 | integrity sha512-QOWH24q+6Z0bvYjuiQ5rNlJMWTbogsQkgL1EV35n0jy9msh2I9bgxwryGAFE/N6w8FWQJR+8p4/mIT7+nAb43g== 14 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: KMP Pull Request 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | 14 | validate: 15 | strategy: 16 | matrix: 17 | os: [ macOS-latest, ubuntu-latest ] 18 | 19 | runs-on: ${{matrix.os}} 20 | steps: 21 | - uses: actions/checkout@v3 22 | - name: Set up JDK ${{ matrix.version }} 23 | uses: actions/setup-java@v3 24 | with: 25 | java-version: 17 26 | distribution: 'zulu' 27 | - name: Run tests for Linux, JVM, and Javascript 28 | if: matrix.os == 'ubuntu-latest' 29 | run: ./gradlew linuxX64Test jvmTest jsTest 30 | - name: Run test on iOS 31 | if: matrix.os == 'macOS-latest' 32 | run: ./gradlew iosX64Test macOSX64Test 33 | - uses: codecov/codecov-action@v3 34 | if: matrix.os == 'ubuntu-latest' 35 | -------------------------------------------------------------------------------- /adhan/src/commonMain/kotlin/com/batoulapps/adhan2/internal/QiblaUtil.kt: -------------------------------------------------------------------------------- 1 | package com.batoulapps.adhan2.internal 2 | 3 | import com.batoulapps.adhan2.Coordinates 4 | import kotlin.math.atan2 5 | import kotlin.math.cos 6 | import kotlin.math.sin 7 | import kotlin.math.tan 8 | 9 | object QiblaUtil { 10 | private val MAKKAH = Coordinates(21.4225241, 39.8261818) 11 | 12 | fun calculateQiblaDirection(coordinates: Coordinates): Double { 13 | // Equation from "Spherical Trigonometry For the use of colleges and schools" page 50 14 | val longitudeDelta: Double = MAKKAH.longitude.toRadians() - coordinates.longitude.toRadians() 15 | val latitudeRadians: Double = coordinates.latitude.toRadians() 16 | val term1: Double = sin(longitudeDelta) 17 | val term2: Double = cos(latitudeRadians) * tan(MAKKAH.latitude.toRadians()) 18 | 19 | val term3: Double = sin(latitudeRadians) * cos(longitudeDelta) 20 | val angle: Double = atan2(term1, term2 - term3) 21 | return DoubleUtil.unwindAngle(angle.toDegrees()) 22 | } 23 | } -------------------------------------------------------------------------------- /adhan/src/commonMain/kotlin/com/batoulapps/adhan2/data/TimeComponents.kt: -------------------------------------------------------------------------------- 1 | package com.batoulapps.adhan2.data 2 | 3 | import kotlinx.datetime.DateTimeUnit 4 | import kotlinx.datetime.LocalDateTime 5 | import kotlin.math.floor 6 | 7 | class TimeComponents private constructor(val hours: Int, val minutes: Int, val seconds: Int) { 8 | 9 | fun dateComponents(date: DateComponents): LocalDateTime { 10 | val localDateTime = LocalDateTime(date.year, date.month, date.day, 0, minutes, seconds) 11 | return CalendarUtil.add(localDateTime, hours, DateTimeUnit.HOUR) 12 | } 13 | 14 | companion object { 15 | fun fromDouble(value: Double): TimeComponents? { 16 | if (value.isInfinite() || value.isNaN()) { 17 | return null 18 | } 19 | val hours: Double = floor(value) 20 | val minutes: Double = floor((value - hours) * 60.0) 21 | val seconds: Double = floor((value - (hours + minutes / 60.0)) * 60 * 60) 22 | return TimeComponents(hours.toInt(), minutes.toInt(), seconds.toInt()) 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## development 4 | 5 | ## version 0.0.6 6 | - update Kotlin to 2.2.20 7 | - update kotlinx-datetime (thanks @fathonyfath) 8 | - support wasm target 9 | - use vanniktech Gradle Maven Publish Plugin 10 | 11 | ## version 0.0.5 12 | - update Kotlin to 1.9.22 13 | - new target: Linux arm64 14 | - support Turkish Diyanet method 15 | - support Shafaq parameter 16 | - support rounding, and update Singapore method to use up rounding 17 | - update test data to [d418fb3](https://github.com/batoulapps/adhan-testdata/commit/d418fb37b3d011af5594e344c06c0e5616db2a5c). 18 | - add a plethora of tests 19 | 20 | ## version 0.0.4 21 | - support macOS arm64, Linux X64, Windows X64, JS, and watchOS 22 | - update to Kotlin 1.7.10 23 | - update kotlinx-datetime to 0.4.0 24 | 25 | ## version 0.0.3 26 | - update to Kotlin 1.6.10 27 | - support for Apple Silicon simulators 28 | 29 | ## version 0.0.2 30 | - update to Kotlin 1.5.21 31 | - rename package to com.batoulapps.adhan2 to allow coexisting with adhan 32 | 33 | ## version 0.0.1 34 | - initial release using KMP 35 | -------------------------------------------------------------------------------- /adhan/src/commonMain/kotlin/com/batoulapps/adhan2/HighLatitudeRule.kt: -------------------------------------------------------------------------------- 1 | package com.batoulapps.adhan2 2 | 3 | /** 4 | * Rules for dealing with Fajr and Isha at places with high latitudes 5 | */ 6 | enum class HighLatitudeRule { 7 | /** 8 | * Fajr will never be earlier than the middle of the night, and Isha will never be later than 9 | * the middle of the night. 10 | */ 11 | MIDDLE_OF_THE_NIGHT, 12 | 13 | /** 14 | * Fajr will never be earlier than the beginning of the last seventh of the night, and Isha will 15 | * never be later than the end of hte first seventh of the night. 16 | */ 17 | SEVENTH_OF_THE_NIGHT, 18 | 19 | /** 20 | * Similar to [HighLatitudeRule.SEVENTH_OF_THE_NIGHT], but instead of 1/7th, the faction 21 | * of the night used is fajrAngle / 60 and ishaAngle/60. 22 | */ 23 | TWILIGHT_ANGLE; 24 | 25 | companion object { 26 | fun recommendedFor(coordinates: Coordinates): HighLatitudeRule { 27 | return if (coordinates.latitude > 48.0) { 28 | HighLatitudeRule.SEVENTH_OF_THE_NIGHT 29 | } else { 30 | HighLatitudeRule.MIDDLE_OF_THE_NIGHT 31 | } 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Batoul Apps 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /adhan/src/commonMain/kotlin/com/batoulapps/adhan2/data/DateComponents.kt: -------------------------------------------------------------------------------- 1 | package com.batoulapps.adhan2.data 2 | 3 | import kotlinx.datetime.LocalDateTime 4 | import kotlinx.datetime.TimeZone 5 | import kotlinx.datetime.number 6 | import kotlinx.datetime.toLocalDateTime 7 | import kotlin.time.Instant 8 | 9 | class DateComponents(val year: Int, val month: Int, val day: Int) { 10 | companion object { 11 | /** 12 | * Convenience method that returns a DateComponents from a given [Instant] 13 | * @param instant the current instant 14 | * @return the [DateComponents] (according to the default device timezone) 15 | */ 16 | fun from(instant: Instant): DateComponents { 17 | val localDateTime = instant.toLocalDateTime(TimeZone.currentSystemDefault()) 18 | return fromLocalDateTime(localDateTime) 19 | } 20 | 21 | /** 22 | * Convenience method that returns a DateComponents from a given [LocalDateTime] 23 | * @param date the date 24 | * @return the DateComponents 25 | */ 26 | fun fromLocalDateTime(date: LocalDateTime): DateComponents { 27 | return DateComponents(date.year, date.month.number, date.day) 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx1536m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official 22 | version=0.0.6 23 | -------------------------------------------------------------------------------- /samples/src/main/java/com/batoulapps/adhan2/Example.kt: -------------------------------------------------------------------------------- 1 | package com.batoulapps.adhan2 2 | 3 | import com.batoulapps.adhan2.data.DateComponents 4 | import java.text.SimpleDateFormat 5 | import java.util.Date 6 | import java.util.TimeZone 7 | import kotlin.time.Clock.System 8 | import kotlin.time.Instant 9 | 10 | object Example { 11 | private fun Instant.asDate() = Date(toEpochMilliseconds()) 12 | 13 | @JvmStatic 14 | fun main(args: Array) { 15 | // get prayer times in Makkah 16 | val coordinates = Coordinates(21.4359571, 39.7064646) 17 | val dateComponents: DateComponents = DateComponents.from(System.now()) 18 | val parameters: CalculationParameters = CalculationMethod.UMM_AL_QURA.parameters 19 | 20 | val prayerTimes = PrayerTimes(coordinates, dateComponents, parameters) 21 | 22 | val formatter = SimpleDateFormat("hh:mm a") 23 | formatter.timeZone = TimeZone.getTimeZone("Asia/Riyadh") 24 | 25 | println("Fajr: " + formatter.format(prayerTimes.fajr.asDate())) 26 | println("Sunrise: " + formatter.format(prayerTimes.sunrise.asDate())) 27 | println("Dhuhr: " + formatter.format(prayerTimes.dhuhr.asDate())) 28 | println("Asr: " + formatter.format(prayerTimes.asr.asDate())) 29 | println("Maghrib: " + formatter.format(prayerTimes.maghrib.asDate())) 30 | println("Isha: " + formatter.format(prayerTimes.isha.asDate())) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /adhan/src/commonTest/kotlin/com/batoulapps/adhan2/CalculationParametersTest.kt: -------------------------------------------------------------------------------- 1 | package com.batoulapps.adhan2 2 | 3 | import com.batoulapps.adhan2.HighLatitudeRule.MIDDLE_OF_THE_NIGHT 4 | import com.batoulapps.adhan2.HighLatitudeRule.SEVENTH_OF_THE_NIGHT 5 | import com.batoulapps.adhan2.HighLatitudeRule.TWILIGHT_ANGLE 6 | import kotlin.math.abs 7 | import kotlin.test.Test 8 | import kotlin.test.assertTrue 9 | 10 | class CalculationParametersTest { 11 | 12 | @Test 13 | fun testNightPortion() { 14 | var parameters = CalculationParameters(fajrAngle = 18.0, ishaAngle = 18.0, highLatitudeRule = MIDDLE_OF_THE_NIGHT) 15 | val coordinates = Coordinates(latitude = 0.0, longitude = 0.0) 16 | assertTrue { abs(parameters.nightPortions(coordinates).fajr - 0.5) <= 0.001 } 17 | assertTrue { abs(parameters.nightPortions(coordinates).isha - 0.5) <= 0.001 } 18 | 19 | parameters = CalculationParameters(fajrAngle = 18.0, ishaAngle = 18.0, highLatitudeRule = SEVENTH_OF_THE_NIGHT) 20 | assertTrue { abs(parameters.nightPortions(coordinates).fajr - (1.0 / 7.0)) <= 0.001 } 21 | assertTrue { abs(parameters.nightPortions(coordinates).isha - (1.0 / 7.0)) <= 0.001 } 22 | 23 | parameters = CalculationParameters(fajrAngle = 10.0, ishaAngle = 15.0, highLatitudeRule = TWILIGHT_ANGLE) 24 | assertTrue { abs(parameters.nightPortions(coordinates).fajr - (10.0 / 60.0)) <= 0.001 } 25 | assertTrue { abs(parameters.nightPortions(coordinates).isha - (15.0 / 60.0)) <= 0.001 } 26 | } 27 | } -------------------------------------------------------------------------------- /adhan/src/commonMain/kotlin/com/batoulapps/adhan2/SunnahTimes.kt: -------------------------------------------------------------------------------- 1 | package com.batoulapps.adhan2 2 | 3 | import com.batoulapps.adhan2.data.CalendarUtil 4 | import com.batoulapps.adhan2.data.CalendarUtil.add 5 | import com.batoulapps.adhan2.data.CalendarUtil.roundedMinute 6 | import com.batoulapps.adhan2.data.CalendarUtil.toUtcInstant 7 | import com.batoulapps.adhan2.data.DateComponents 8 | import kotlinx.datetime.DateTimeUnit 9 | import kotlin.time.Instant 10 | 11 | class SunnahTimes(prayerTimes: PrayerTimes) { 12 | /* The midpoint between Maghrib and Fajr */ 13 | val middleOfTheNight: Instant 14 | 15 | /* The beginning of the last third of the period between Maghrib and Fajr, 16 | a recommended time to perform Qiyam */ 17 | val lastThirdOfTheNight: Instant 18 | 19 | init { 20 | val currentPrayerTimesDate = CalendarUtil.resolveTime(prayerTimes.dateComponents) 21 | val tomorrowPrayerTimesDate = add(currentPrayerTimesDate, 1, DateTimeUnit.DAY) 22 | val tomorrowPrayerTimes = prayerTimes.copy(dateComponents = DateComponents.fromLocalDateTime(tomorrowPrayerTimesDate)) 23 | 24 | val nightDurationInSeconds = 25 | (tomorrowPrayerTimes.fajr.toEpochMilliseconds() - 26 | prayerTimes.maghrib.toEpochMilliseconds()) / 1000 27 | middleOfTheNight = roundedMinute( 28 | add(prayerTimes.maghrib, (nightDurationInSeconds / 2.0).toInt(), DateTimeUnit.SECOND) 29 | ).toUtcInstant() 30 | lastThirdOfTheNight = roundedMinute( 31 | add( 32 | prayerTimes.maghrib, 33 | (nightDurationInSeconds * (2.0 / 3.0)).toInt(), 34 | DateTimeUnit.SECOND 35 | ) 36 | ).toUtcInstant() 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /adhan/src/commonTest/kotlin/com/batoulapps/adhan2/internal/TestUtils.kt: -------------------------------------------------------------------------------- 1 | package com.batoulapps.adhan2.internal 2 | 3 | import com.batoulapps.adhan2.data.CalendarUtil 4 | import com.batoulapps.adhan2.data.CalendarUtil.toUtcInstant 5 | import com.batoulapps.adhan2.data.DateComponents 6 | import kotlinx.datetime.DateTimeUnit 7 | import kotlinx.datetime.LocalDateTime 8 | import kotlinx.datetime.TimeZone 9 | import kotlinx.datetime.plus 10 | import kotlinx.datetime.toInstant 11 | import kotlinx.datetime.toLocalDateTime 12 | import kotlin.time.Instant 13 | 14 | object TestUtils { 15 | 16 | fun makeDate( 17 | year: Int, 18 | month: Int, 19 | day: Int, 20 | hour: Int = 0, 21 | minute: Int = 0, 22 | second: Int = 0 23 | ) = LocalDateTime( 24 | year = year, 25 | month = month, 26 | day = day, 27 | hour = hour, 28 | minute = minute, 29 | second = second 30 | ) 31 | 32 | fun pad(value: Int): String { 33 | return if (value < 10) { 34 | "0$value" 35 | } else { 36 | value.toString() 37 | } 38 | } 39 | 40 | fun getDateComponents(date: String): DateComponents { 41 | val pieces = date.split("-").toTypedArray() 42 | val year = pieces[0].toInt() 43 | val month = pieces[1].toInt() 44 | val day = pieces[2].toInt() 45 | return DateComponents(year, month, day) 46 | } 47 | 48 | fun addSeconds(gmtInstant: Instant, offset: Int): Instant { 49 | return CalendarUtil.add(gmtInstant, offset, DateTimeUnit.SECOND).toUtcInstant() 50 | } 51 | 52 | fun makeDateWithOffset(year: Int, month: Int, day: Int, offset: Int, dateTimeUnit: DateTimeUnit): DateComponents { 53 | val localDateTime = LocalDateTime(year = year, month = month, day = day, hour = 0, minute = 0) 54 | val instant = localDateTime.toInstant(TimeZone.UTC) 55 | val updatedInstant = instant.plus(offset, dateTimeUnit, TimeZone.UTC) 56 | val updatedLocalDateTime = updatedInstant.toLocalDateTime(TimeZone.UTC) 57 | return DateComponents.fromLocalDateTime(updatedLocalDateTime) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /adhan/src/commonMain/kotlin/com/batoulapps/adhan2/internal/CalendricalHelper.kt: -------------------------------------------------------------------------------- 1 | package com.batoulapps.adhan2.internal 2 | 3 | import kotlinx.datetime.LocalDateTime 4 | import kotlinx.datetime.TimeZone 5 | import kotlinx.datetime.number 6 | import kotlinx.datetime.toLocalDateTime 7 | import kotlin.time.Instant 8 | 9 | object CalendricalHelper { 10 | /** 11 | * The Julian Day for a given date 12 | * @param instant the instant 13 | * @return the julian day 14 | */ 15 | fun julianDay(instant: Instant): Double { 16 | val localDateTime = instant.toLocalDateTime(TimeZone.UTC) 17 | return julianDay(localDateTime) 18 | } 19 | 20 | /** 21 | * The Julian Day for a given date 22 | * @param date a UTC date 23 | * @return the julian day 24 | */ 25 | fun julianDay(date: LocalDateTime): Double { 26 | return julianDay( 27 | date.year, date.month.number, date.day, 28 | date.hour + date.minute / 60.0 29 | ) 30 | } 31 | 32 | /** 33 | * The Julian Day for a given Gregorian date 34 | * @param year the year 35 | * @param month the month 36 | * @param day the day 37 | * @param hours hours 38 | * @return the julian day 39 | */ 40 | fun julianDay(year: Int, month: Int, day: Int, hours: Double = 0.0): Double { 41 | /* Equation from Astronomical Algorithms page 60 */ 42 | 43 | // NOTE: Integer conversion is done intentionally for the purpose of decimal truncation 44 | val Y = if (month > 2) year else year - 1 45 | val M = if (month > 2) month else month + 12 46 | val D = day + hours / 24 47 | val A = Y / 100 48 | val B = 2 - A + A / 4 49 | val i0 = (365.25 * (Y + 4716)).toInt() 50 | val i1 = (30.6001 * (M + 1)).toInt() 51 | return i0 + i1 + D + B - 1524.5 52 | } 53 | 54 | /** 55 | * Julian century from the epoch. 56 | * @param JD the julian day 57 | * @return the julian century from the epoch 58 | */ 59 | fun julianCentury(JD: Double): Double { 60 | /* Equation from Astronomical Algorithms page 163 */ 61 | return (JD - 2451545.0) / 36525 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /adhan/src/commonMain/kotlin/com/batoulapps/adhan2/CalculationParameters.kt: -------------------------------------------------------------------------------- 1 | package com.batoulapps.adhan2 2 | 3 | import com.batoulapps.adhan2.model.Rounding 4 | import com.batoulapps.adhan2.model.Shafaq 5 | 6 | /** 7 | * Parameters used for PrayerTime calculation customization 8 | * 9 | * Note that, for many cases, you can use {@link CalculationMethod#getParameters()} to get a 10 | * pre-computed set of calculation parameters depending on one of the available 11 | * {@link CalculationMethod}. 12 | */ 13 | data class CalculationParameters( 14 | // The angle of the sun used to calculate fajr 15 | val fajrAngle: Double = 0.0, 16 | 17 | // The angle of the sun used to calculate isha 18 | val ishaAngle: Double = 0.0, 19 | 20 | // Minutes after Maghrib (if set, the time for Isha will be Maghrib plus IshaInterval) 21 | val ishaInterval: Int = 0, 22 | 23 | // The method used to do the calculation 24 | val method: CalculationMethod = CalculationMethod.OTHER, 25 | 26 | // The madhab used to calculate Asr 27 | val madhab: Madhab = Madhab.SHAFI, 28 | 29 | // Rules for placing bounds on Fajr and Isha for high latitude areas 30 | val highLatitudeRule: HighLatitudeRule? = null, 31 | 32 | // Used to optionally add or subtract a set amount of time from each prayer time 33 | val prayerAdjustments: PrayerAdjustments = PrayerAdjustments(), 34 | 35 | // Used for method adjustments 36 | val methodAdjustments: PrayerAdjustments = PrayerAdjustments(), 37 | 38 | // Rounding 39 | val rounding: Rounding = Rounding.NEAREST, 40 | 41 | // Twilight in the sky 42 | val shafaq: Shafaq = Shafaq.GENERAL 43 | ) { 44 | 45 | data class NightPortions(val fajr: Double, val isha: Double) 46 | 47 | fun nightPortions(coordinates: Coordinates): NightPortions { 48 | return when (highLatitudeRule ?: HighLatitudeRule.recommendedFor(coordinates)) { 49 | HighLatitudeRule.MIDDLE_OF_THE_NIGHT -> { 50 | NightPortions(1.0 / 2.0, 1.0 / 2.0) 51 | } 52 | HighLatitudeRule.SEVENTH_OF_THE_NIGHT -> { 53 | NightPortions(1.0 / 7.0, 1.0 / 7.0) 54 | } 55 | HighLatitudeRule.TWILIGHT_ANGLE -> { 56 | NightPortions(this.fajrAngle / 60.0, this.ishaAngle / 60.0) 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /adhan/src/commonTest/kotlin/com/batoulapps/adhan2/QiblaTest.kt: -------------------------------------------------------------------------------- 1 | package com.batoulapps.adhan2 2 | 3 | import kotlin.math.abs 4 | import kotlin.test.Test 5 | import kotlin.test.assertTrue 6 | 7 | class QiblaTest { 8 | private fun Double.isWithin(threshold: Double) = this to threshold 9 | private fun Pair.of(value: Double) = abs(first - value) <= second 10 | 11 | @Test 12 | fun testNorthAmerica() { 13 | val washingtonDC = Coordinates(38.9072, -77.0369) 14 | assertTrue(Qibla(washingtonDC).direction.isWithin(0.001).of(56.560)) 15 | val nyc = Coordinates(40.7128, -74.0059) 16 | assertTrue(Qibla(nyc).direction.isWithin(0.001).of(58.481)) 17 | val sanFrancisco = Coordinates(37.7749, -122.4194) 18 | assertTrue(Qibla(sanFrancisco).direction.isWithin(0.001).of(18.843)) 19 | val anchorage = Coordinates(61.2181, -149.9003) 20 | assertTrue(Qibla(anchorage).direction.isWithin(0.001).of(350.883)) 21 | } 22 | 23 | @Test 24 | fun testSouthPacific() { 25 | val sydney = Coordinates(-33.8688, 151.2093) 26 | assertTrue(Qibla(sydney).direction.isWithin(0.001).of(277.499)) 27 | val auckland = Coordinates(-36.8485, 174.7633) 28 | assertTrue(Qibla(auckland).direction.isWithin(0.001).of(261.197)) 29 | } 30 | 31 | @Test 32 | fun testEurope() { 33 | val london = Coordinates(51.5074, -0.1278) 34 | assertTrue(Qibla(london).direction.isWithin(0.001).of(118.987)) 35 | val paris = Coordinates(48.8566, 2.3522) 36 | assertTrue(Qibla(paris).direction.isWithin(0.001).of(119.163)) 37 | val oslo = Coordinates(59.9139, 10.7522) 38 | assertTrue(Qibla(oslo).direction.isWithin(0.001).of(139.027)) 39 | } 40 | 41 | @Test 42 | fun testAsia() { 43 | val islamabad = Coordinates(33.7294, 73.0931) 44 | assertTrue(Qibla(islamabad).direction.isWithin(0.001).of(255.882)) 45 | val tokyo = Coordinates(35.6895, 139.6917) 46 | assertTrue(Qibla(tokyo).direction.isWithin(0.001).of(293.021)) 47 | } 48 | 49 | @Test 50 | fun testAfrica() { 51 | val capeTown = Coordinates(33.9249, 18.4241) 52 | assertTrue(Qibla(capeTown).direction.isWithin(0.001).of(118.004)) 53 | val cairo = Coordinates(30.0444, 31.2357) 54 | assertTrue(Qibla(cairo).direction.isWithin(0.001).of(136.137)) 55 | } 56 | } -------------------------------------------------------------------------------- /adhan/src/commonMain/kotlin/com/batoulapps/adhan2/internal/SolarTime.kt: -------------------------------------------------------------------------------- 1 | package com.batoulapps.adhan2.internal 2 | 3 | import com.batoulapps.adhan2.Coordinates 4 | import com.batoulapps.adhan2.data.DateComponents 5 | import com.batoulapps.adhan2.internal.Astronomical.approximateTransit 6 | import com.batoulapps.adhan2.internal.Astronomical.correctedHourAngle 7 | import com.batoulapps.adhan2.internal.Astronomical.correctedTransit 8 | import kotlin.math.abs 9 | import kotlin.math.atan 10 | import kotlin.math.tan 11 | 12 | class SolarTime(date: DateComponents, coordinates: Coordinates) { 13 | val transit: Double 14 | val sunrise: Double 15 | val sunset: Double 16 | 17 | private val observer: Coordinates 18 | private val solar: SolarCoordinates 19 | private val prevSolar: SolarCoordinates 20 | private val nextSolar: SolarCoordinates 21 | private val approximateTransit: Double 22 | 23 | init { 24 | val julianDate = CalendricalHelper.julianDay(date.year, date.month, date.day) 25 | prevSolar = SolarCoordinates(julianDate - 1) 26 | solar = SolarCoordinates(julianDate) 27 | nextSolar = SolarCoordinates(julianDate + 1) 28 | approximateTransit = approximateTransit( 29 | coordinates.longitude, 30 | solar.apparentSiderealTime, solar.rightAscension 31 | ) 32 | val solarAltitude = -50.0 / 60.0 33 | observer = coordinates 34 | transit = correctedTransit( 35 | approximateTransit, coordinates.longitude, 36 | solar.apparentSiderealTime, solar.rightAscension, prevSolar.rightAscension, 37 | nextSolar.rightAscension 38 | ) 39 | sunrise = correctedHourAngle( 40 | approximateTransit, solarAltitude, 41 | coordinates, false, solar.apparentSiderealTime, solar.rightAscension, 42 | prevSolar.rightAscension, nextSolar.rightAscension, solar.declination, 43 | prevSolar.declination, nextSolar.declination 44 | ) 45 | sunset = correctedHourAngle( 46 | approximateTransit, solarAltitude, 47 | coordinates, true, solar.apparentSiderealTime, solar.rightAscension, 48 | prevSolar.rightAscension, nextSolar.rightAscension, solar.declination, 49 | prevSolar.declination, nextSolar.declination 50 | ) 51 | } 52 | 53 | fun timeForSolarAngle(angle: Double, afterTransit: Boolean): Double { 54 | return correctedHourAngle( 55 | approximateTransit, angle, coordinates = observer, 56 | afterTransit, solar.apparentSiderealTime, solar.rightAscension, 57 | prevSolar.rightAscension, nextSolar.rightAscension, solar.declination, 58 | prevSolar.declination, nextSolar.declination 59 | ) 60 | } 61 | 62 | // hours from transit 63 | fun afternoon(shadowLength: ShadowLength): Double { 64 | // TODO (from Swift version) source shadow angle calculation 65 | val tangent: Double = abs(observer.latitude - solar.declination) 66 | val inverse: Double = 67 | shadowLength.shadowLength + tan(tangent.toRadians()) 68 | val angle: Double = atan(1.0 / inverse).toDegrees() 69 | return timeForSolarAngle(angle, true) 70 | } 71 | } -------------------------------------------------------------------------------- /adhan/src/commonMain/kotlin/com/batoulapps/adhan2/internal/SolarCoordinates.kt: -------------------------------------------------------------------------------- 1 | package com.batoulapps.adhan2.internal 2 | 3 | import com.batoulapps.adhan2.internal.Astronomical.apparentObliquityOfTheEcliptic 4 | import com.batoulapps.adhan2.internal.Astronomical.apparentSolarLongitude 5 | import com.batoulapps.adhan2.internal.Astronomical.ascendingLunarNodeLongitude 6 | import com.batoulapps.adhan2.internal.Astronomical.meanLunarLongitude 7 | import com.batoulapps.adhan2.internal.Astronomical.meanObliquityOfTheEcliptic 8 | import com.batoulapps.adhan2.internal.Astronomical.meanSiderealTime 9 | import com.batoulapps.adhan2.internal.Astronomical.meanSolarLongitude 10 | import com.batoulapps.adhan2.internal.Astronomical.nutationInLongitude 11 | import com.batoulapps.adhan2.internal.Astronomical.nutationInObliquity 12 | import com.batoulapps.adhan2.internal.DoubleUtil.unwindAngle 13 | import kotlin.math.asin 14 | import kotlin.math.atan2 15 | import kotlin.math.cos 16 | import kotlin.math.sin 17 | 18 | internal class SolarCoordinates(julianDay: Double) { 19 | /** 20 | * The declination of the sun, the angle between 21 | * the rays of the Sun and the plane of the Earth's 22 | * equator, in degrees. 23 | */ 24 | val declination: Double 25 | 26 | /** 27 | * Right ascension of the Sun, the angular distance on the 28 | * celestial equator from the vernal equinox to the hour circle, 29 | * in degrees. 30 | */ 31 | val rightAscension: Double 32 | 33 | /** 34 | * Apparent sidereal time, the hour angle of the vernal 35 | * equinox, in degrees. 36 | */ 37 | val apparentSiderealTime: Double 38 | 39 | init { 40 | val T: Double = CalendricalHelper.julianCentury(julianDay) 41 | val L0 = meanSolarLongitude( /* julianCentury */T) 42 | val Lp = meanLunarLongitude( /* julianCentury */T) 43 | val Ω = ascendingLunarNodeLongitude( /* julianCentury */T) 44 | val λ: Double = apparentSolarLongitude( /* julianCentury*/T, /* meanLongitude */L0).toRadians() 45 | val θ0 = meanSiderealTime( /* julianCentury */T) 46 | val ΔΨ = 47 | nutationInLongitude( /* julianCentury */T, /* solarLongitude */L0, /* lunarLongitude */ 48 | Lp, /* ascendingNode */Ω 49 | ) 50 | val Δε = 51 | nutationInObliquity( /* julianCentury */T, /* solarLongitude */L0, /* lunarLongitude */ 52 | Lp, /* ascendingNode */Ω 53 | ) 54 | val ε0 = meanObliquityOfTheEcliptic( /* julianCentury */T) 55 | val εapp: Double = 56 | apparentObliquityOfTheEcliptic( /* julianCentury */ 57 | T, /* meanObliquityOfTheEcliptic */ε0 58 | ).toRadians() 59 | 60 | /* Equation from Astronomical Algorithms page 165 */ 61 | declination = 62 | asin(sin(εapp) * sin(λ)).toDegrees() 63 | 64 | /* Equation from Astronomical Algorithms page 165 */ 65 | rightAscension = unwindAngle( 66 | atan2(cos(εapp) * sin(λ), cos(λ)).toDegrees() 67 | ) 68 | 69 | /* Equation from Astronomical Algorithms page 88 */ 70 | apparentSiderealTime = 71 | θ0 + ΔΨ * 3600 * cos((ε0 + Δε).toRadians()) / 3600 72 | } 73 | } -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /adhan/src/commonTest/kotlin/com/batoulapps/adhan2/CalculationMethodTest.kt: -------------------------------------------------------------------------------- 1 | package com.batoulapps.adhan2 2 | 3 | import com.batoulapps.adhan2.CalculationMethod.DUBAI 4 | import com.batoulapps.adhan2.CalculationMethod.EGYPTIAN 5 | import com.batoulapps.adhan2.CalculationMethod.KARACHI 6 | import com.batoulapps.adhan2.CalculationMethod.KUWAIT 7 | import com.batoulapps.adhan2.CalculationMethod.MOON_SIGHTING_COMMITTEE 8 | import com.batoulapps.adhan2.CalculationMethod.MUSLIM_WORLD_LEAGUE 9 | import com.batoulapps.adhan2.CalculationMethod.NORTH_AMERICA 10 | import com.batoulapps.adhan2.CalculationMethod.OTHER 11 | import com.batoulapps.adhan2.CalculationMethod.QATAR 12 | import com.batoulapps.adhan2.CalculationMethod.UMM_AL_QURA 13 | import kotlin.math.abs 14 | import kotlin.test.Test 15 | import kotlin.test.assertEquals 16 | import kotlin.test.assertTrue 17 | 18 | class CalculationMethodTest { 19 | 20 | @Test 21 | fun testCalculationMethods() { 22 | var params = MUSLIM_WORLD_LEAGUE.parameters 23 | assertTrue { abs(params.fajrAngle - 18) <= 0.000001 } 24 | assertTrue { abs(params.ishaAngle - 17) <= 0.000001 } 25 | assertEquals(0, params.ishaInterval) 26 | assertEquals(MUSLIM_WORLD_LEAGUE, params.method) 27 | 28 | params = EGYPTIAN.parameters 29 | assertTrue { abs(params.fajrAngle - 19.5) <= 0.000001 } 30 | assertTrue { abs(params.ishaAngle - 17.5) <= 0.000001 } 31 | assertEquals(0, params.ishaInterval) 32 | assertEquals(EGYPTIAN, params.method) 33 | 34 | params = KARACHI.parameters 35 | assertTrue { abs(params.fajrAngle - 18) <= 0.000001 } 36 | assertTrue { abs(params.ishaAngle - 18) <= 0.000001 } 37 | assertEquals(0, params.ishaInterval) 38 | assertEquals(KARACHI, params.method) 39 | 40 | params = UMM_AL_QURA.parameters 41 | assertTrue { abs(params.fajrAngle - 18.5) <= 0.000001 } 42 | assertTrue { abs(params.ishaAngle - 0) <= 0.000001 } 43 | assertEquals(90, params.ishaInterval) 44 | assertEquals(UMM_AL_QURA, params.method) 45 | 46 | params = DUBAI.parameters 47 | assertTrue { abs(params.fajrAngle - 18.2) <= 0.000001 } 48 | assertTrue { abs(params.ishaAngle - 18.2) <= 0.000001 } 49 | assertEquals(0, params.ishaInterval) 50 | assertEquals(DUBAI, params.method) 51 | 52 | params = MOON_SIGHTING_COMMITTEE.parameters 53 | assertTrue { abs(params.fajrAngle - 18) <= 0.000001 } 54 | assertTrue { abs(params.ishaAngle - 18) <= 0.000001 } 55 | assertEquals(0, params.ishaInterval) 56 | assertEquals(MOON_SIGHTING_COMMITTEE, params.method) 57 | 58 | params = NORTH_AMERICA.parameters 59 | assertTrue { abs(params.fajrAngle - 15) <= 0.000001 } 60 | assertTrue { abs(params.ishaAngle - 15) <= 0.000001 } 61 | assertEquals(0, params.ishaInterval) 62 | assertEquals(NORTH_AMERICA, params.method) 63 | 64 | params = KUWAIT.parameters 65 | assertTrue { abs(params.fajrAngle - 18) <= 0.000001 } 66 | assertTrue { abs(params.ishaAngle - 17.5) <= 0.000001 } 67 | assertEquals(0, params.ishaInterval) 68 | assertEquals(KUWAIT, params.method) 69 | 70 | params = QATAR.parameters 71 | assertTrue { abs(params.fajrAngle - 18) <= 0.000001 } 72 | assertTrue { abs(params.ishaAngle - 0) <= 0.000001 } 73 | assertEquals(90, params.ishaInterval) 74 | assertEquals(QATAR, params.method) 75 | 76 | params = OTHER.parameters 77 | assertTrue { abs(params.fajrAngle - 0) <= 0.000001 } 78 | assertTrue { abs(params.ishaAngle - 0) <= 0.000001 } 79 | assertEquals(0, params.ishaInterval) 80 | assertEquals(OTHER, params.method) 81 | } 82 | } -------------------------------------------------------------------------------- /Shared/Times/Kuwait City-Kuwait.json: -------------------------------------------------------------------------------- 1 | { 2 | "params": { 3 | "latitude": 29.370865, 4 | "longitude": 47.979139, 5 | "timezone": "Asia/Kuwait", 6 | "method": "Kuwait", 7 | "madhab": "Shafi", 8 | "highLatitudeRule": "MiddleOfTheNight" 9 | }, 10 | "source": [ 11 | "https://itunes.apple.com/us/app/kuwait-prayer-times/id723108544?mt=12", 12 | "adjusted +/- 2 minutes", 13 | "1/1 0518 0642 1151 1442 1701 1823", 14 | "1/2 0516 0638 1202 1505 1726 1845", 15 | "1/3 0456 0615 1201 1519 1747 1904", 16 | "1/4 0418 0538 1152 1523 1806 1924", 17 | "1/5 0341 0507 1145 1521 1824 1947", 18 | "1/6 0315 0449 1146 1520 1843 2013", 19 | "1/7 0316 0452 1152 1526 1852 2023", 20 | "1/8 0339 0508 1154 1530 1840 2006", 21 | "1/9 0403 0525 1148 1522 1811 1930", 22 | "1/10 0422 0541 1138 1502 1734 1851", 23 | "1/11 0440 0601 1132 1439 1702 1820", 24 | "1/12 0501 0624 1137 1430 1649 1811" 25 | ], 26 | "variance": 2, 27 | "times": [ 28 | { 29 | "date": "2016-01-01", 30 | "fajr": "5:18 AM", 31 | "sunrise": "6:42 AM", 32 | "dhuhr": "11:51 AM", 33 | "asr": "2:42 PM", 34 | "maghrib": "5:01 PM", 35 | "isha": "6:23 PM" 36 | }, 37 | { 38 | "date": "2016-02-01", 39 | "fajr": "5:16 AM", 40 | "sunrise": "6:38 AM", 41 | "dhuhr": "12:02 PM", 42 | "asr": "3:05 PM", 43 | "maghrib": "5:26 PM", 44 | "isha": "6:45 PM" 45 | }, 46 | { 47 | "date": "2016-03-01", 48 | "fajr": "4:56 AM", 49 | "sunrise": "6:15 AM", 50 | "dhuhr": "12:01 PM", 51 | "asr": "3:19 PM", 52 | "maghrib": "5:47 PM", 53 | "isha": "7:04 PM" 54 | }, 55 | { 56 | "date": "2016-04-01", 57 | "fajr": "4:18 AM", 58 | "sunrise": "5:38 AM", 59 | "dhuhr": "11:52 AM", 60 | "asr": "3:23 PM", 61 | "maghrib": "6:06 PM", 62 | "isha": "7:24 PM" 63 | }, 64 | { 65 | "date": "2016-05-01", 66 | "fajr": "3:41 AM", 67 | "sunrise": "5:07 AM", 68 | "dhuhr": "11:45 AM", 69 | "asr": "3:21 PM", 70 | "maghrib": "6:24 PM", 71 | "isha": "7:47 PM" 72 | }, 73 | { 74 | "date": "2016-06-01", 75 | "fajr": "3:15 AM", 76 | "sunrise": "4:49 AM", 77 | "dhuhr": "11:46 AM", 78 | "asr": "3:20 PM", 79 | "maghrib": "6:43 PM", 80 | "isha": "8:13 PM" 81 | }, 82 | { 83 | "date": "2016-07-01", 84 | "fajr": "3:16 AM", 85 | "sunrise": "4:52 AM", 86 | "dhuhr": "11:52 AM", 87 | "asr": "3:26 PM", 88 | "maghrib": "6:52 PM", 89 | "isha": "8:23 PM" 90 | }, 91 | { 92 | "date": "2016-08-01", 93 | "fajr": "3:39 AM", 94 | "sunrise": "5:08 AM", 95 | "dhuhr": "11:54 AM", 96 | "asr": "3:30 PM", 97 | "maghrib": "6:40 PM", 98 | "isha": "8:06 PM" 99 | }, 100 | { 101 | "date": "2016-09-01", 102 | "fajr": "4:03 AM", 103 | "sunrise": "5:25 AM", 104 | "dhuhr": "11:48 AM", 105 | "asr": "3:22 PM", 106 | "maghrib": "6:11 PM", 107 | "isha": "7:30 PM" 108 | }, 109 | { 110 | "date": "2016-10-01", 111 | "fajr": "4:22 AM", 112 | "sunrise": "5:41 AM", 113 | "dhuhr": "11:38 AM", 114 | "asr": "3:02 PM", 115 | "maghrib": "5:34 PM", 116 | "isha": "6:51 PM" 117 | }, 118 | { 119 | "date": "2016-11-01", 120 | "fajr": "4:40 AM", 121 | "sunrise": "6:01 AM", 122 | "dhuhr": "11:32 AM", 123 | "asr": "2:39 PM", 124 | "maghrib": "5:02 PM", 125 | "isha": "6:20 PM" 126 | }, 127 | { 128 | "date": "2016-12-01", 129 | "fajr": "5:01 AM", 130 | "sunrise": "6:24 AM", 131 | "dhuhr": "11:37 AM", 132 | "asr": "2:30 PM", 133 | "maghrib": "4:49 PM", 134 | "isha": "6:11 PM" 135 | } 136 | ] 137 | } -------------------------------------------------------------------------------- /Shared/Times/Doha-Qatar.json: -------------------------------------------------------------------------------- 1 | { 2 | "params": { 3 | "latitude": 25.283897, 4 | "longitude": 51.528770, 5 | "timezone": "Asia/Riyadh", 6 | "method": "Qatar", 7 | "madhab": "Shafi", 8 | "highLatitudeRule": "MiddleOfTheNight" 9 | }, 10 | "source": [ 11 | "http://www.islam.gov.qa/", 12 | "adjusted +/- 2 minutes", 13 | "Janurary(1) 4:58 6:19 11:37 2:35 4:56-1 6:26-1", 14 | "February(2) 4:59 6:17 11:47 2:56 5:19-1 6:49-1", 15 | "March(3) 4:41 5:57 11:46 3:07 5:37-1 7:07-1", 16 | "April(4) 4:09-1 5:26-1 11:38 3:06+1 5:51 7:21", 17 | "May(5) 3:37-1 4:58 11:31 3:00 6:05 7:35", 18 | "June(6) 3:16-1 4:44-1 11:32 2:56 6:21-1 7:51-1", 19 | "July(7) 3:18 4:47 11:38 3:01 6:29-1 7:59-1", 20 | "August(8) 3:36+1 5:01 11:40 3:07 6:21-2 7:51-2", 21 | "September(9) 3:56 5:14+1 11:34 3:03 5:54-1 7:24-1", 22 | "October(10) 4:10 5:26 11:24-1 2:48-1 5:22-2 6:52-2", 23 | "November(11) 4:24 5:41+1 11:17 2:29 4:54-1 6:24-1", 24 | "December(12) 4:41+1 6:02+1 11:23 2:23 4:44-1 6:14-1" 25 | ], 26 | "times": [ 27 | { 28 | "date": "2016-01-01", 29 | "fajr": "4:58 AM", 30 | "sunrise": "6:19 AM", 31 | "dhuhr": "11:37 AM", 32 | "asr": "2:35 PM", 33 | "maghrib": "4:55 PM", 34 | "isha": "6:25 PM" 35 | }, 36 | { 37 | "date": "2016-02-01", 38 | "fajr": "4:59 AM", 39 | "sunrise": "6:17 AM", 40 | "dhuhr": "11:47 AM", 41 | "asr": "2:56 PM", 42 | "maghrib": "5:18 PM", 43 | "isha": "6:48 PM" 44 | }, 45 | { 46 | "date": "2016-03-01", 47 | "fajr": "4:41 AM", 48 | "sunrise": "5:57 AM", 49 | "dhuhr": "11:46 AM", 50 | "asr": "3:07 PM", 51 | "maghrib": "5:36 PM", 52 | "isha": "7:06 PM" 53 | }, 54 | { 55 | "date": "2016-04-01", 56 | "fajr": "4:08 AM", 57 | "sunrise": "5:25 AM", 58 | "dhuhr": "11:38 AM", 59 | "asr": "3:07 PM", 60 | "maghrib": "5:51 PM", 61 | "isha": "7:21 PM" 62 | }, 63 | { 64 | "date": "2016-05-01", 65 | "fajr": "3:36 AM", 66 | "sunrise": "4:58 AM", 67 | "dhuhr": "11:31 AM", 68 | "asr": "3:00 PM", 69 | "maghrib": "6:05 PM", 70 | "isha": "7:35 PM" 71 | }, 72 | { 73 | "date": "2016-06-01", 74 | "fajr": "3:15 AM", 75 | "sunrise": "4:43 AM", 76 | "dhuhr": "11:32 AM", 77 | "asr": "2:56 PM", 78 | "maghrib": "6:20 PM", 79 | "isha": "7:50 PM" 80 | }, 81 | { 82 | "date": "2016-07-01", 83 | "fajr": "3:18 AM", 84 | "sunrise": "4:47 AM", 85 | "dhuhr": "11:38 AM", 86 | "asr": "3:01 PM", 87 | "maghrib": "6:28 PM", 88 | "isha": "7:58 PM" 89 | }, 90 | { 91 | "date": "2016-08-01", 92 | "fajr": "3:37 AM", 93 | "sunrise": "5:01 AM", 94 | "dhuhr": "11:40 AM", 95 | "asr": "3:07 PM", 96 | "maghrib": "6:19 PM", 97 | "isha": "7:49 PM" 98 | }, 99 | { 100 | "date": "2016-09-01", 101 | "fajr": "3:56 AM", 102 | "sunrise": "5:15 AM", 103 | "dhuhr": "11:34 AM", 104 | "asr": "3:03 PM", 105 | "maghrib": "5:53 PM", 106 | "isha": "7:23 PM" 107 | }, 108 | { 109 | "date": "2016-10-01", 110 | "fajr": "4:10 AM", 111 | "sunrise": "5:26 AM", 112 | "dhuhr": "11:23 AM", 113 | "asr": "2:47 PM", 114 | "maghrib": "5:20 PM", 115 | "isha": "6:50 PM" 116 | }, 117 | { 118 | "date": "2016-11-01", 119 | "fajr": "4:24 AM", 120 | "sunrise": "5:42 AM", 121 | "dhuhr": "11:17 AM", 122 | "asr": "2:29 PM", 123 | "maghrib": "4:53 PM", 124 | "isha": "6:23 PM" 125 | }, 126 | { 127 | "date": "2016-12-01", 128 | "fajr": "4:42 AM", 129 | "sunrise": "6:03 AM", 130 | "dhuhr": "11:23 AM", 131 | "asr": "2:23 PM", 132 | "maghrib": "4:43 PM", 133 | "isha": "6:13 PM" 134 | } 135 | ] 136 | } -------------------------------------------------------------------------------- /adhan/src/commonMain/kotlin/com/batoulapps/adhan2/data/CalendarUtil.kt: -------------------------------------------------------------------------------- 1 | package com.batoulapps.adhan2.data 2 | 3 | import com.batoulapps.adhan2.model.Rounding 4 | import kotlin.math.ceil 5 | import kotlin.math.roundToInt 6 | import kotlinx.datetime.DateTimeUnit 7 | import kotlinx.datetime.LocalDateTime 8 | import kotlinx.datetime.TimeZone 9 | import kotlinx.datetime.plus 10 | import kotlinx.datetime.toInstant 11 | import kotlinx.datetime.toLocalDateTime 12 | import kotlin.time.Instant 13 | 14 | object CalendarUtil { 15 | /** 16 | * Whether or not a year is a leap year (has 366 days) 17 | * @param year the year 18 | * @return whether or not its a leap year 19 | */ 20 | fun isLeapYear(year: Int): Boolean { 21 | return year % 4 == 0 && !(year % 100 == 0 && year % 400 != 0) 22 | } 23 | 24 | /** 25 | * Date and time with a rounded minute 26 | * This returns a date with the seconds rounded and added to the minute 27 | * @param localDateTime the date and time 28 | * @return the date and time with 0 seconds and minutes including rounded seconds 29 | */ 30 | fun roundedMinute(localDateTime: LocalDateTime, rounding: Rounding = Rounding.NEAREST): LocalDateTime { 31 | val originalMinute = localDateTime.minute 32 | val (minute, second) = when (rounding) { 33 | Rounding.NEAREST -> originalMinute + (localDateTime.second / 60f).roundToInt() to 0 34 | Rounding.UP -> originalMinute + ceil(localDateTime.second / 60f).roundToInt() to 0 35 | Rounding.NONE -> originalMinute to localDateTime.second 36 | } 37 | 38 | val localDateTimeWithOldMinutes = LocalDateTime( 39 | year = localDateTime.year, 40 | month = localDateTime.month, 41 | day = localDateTime.day, 42 | hour = localDateTime.hour, 43 | minute = originalMinute, 44 | second = second 45 | ) 46 | 47 | return if (originalMinute != minute) { 48 | val delta = minute - originalMinute 49 | add(localDateTimeWithOldMinutes, delta, DateTimeUnit.MINUTE) 50 | } else { 51 | localDateTimeWithOldMinutes 52 | } 53 | } 54 | 55 | /** 56 | * Gets a date for the particular date 57 | * @param components the date components 58 | * @return the LocalDateTime with a time set to 00:00:00 at utc 59 | */ 60 | fun resolveTime(components: DateComponents): LocalDateTime { 61 | return resolveTime(components.year, components.month, components.day) 62 | } 63 | 64 | /** 65 | * Add the specified amount of a unit of time to a particular date 66 | * @param localDateTime the original date 67 | * @param amount the amount to add 68 | * @param dateTimeUnit the field to add it to 69 | * @return the date with the offset added 70 | */ 71 | fun add(localDateTime: LocalDateTime, amount: Int, dateTimeUnit: DateTimeUnit): LocalDateTime { 72 | val timezone = TimeZone.UTC 73 | val instant = localDateTime.toInstant(timezone) 74 | return add(instant, amount, dateTimeUnit) 75 | } 76 | 77 | /** 78 | * Add the specified amount of a unit of time to a particular date 79 | * @param instant the gmt instant 80 | * @param amount the amount to add 81 | * @param dateTimeUnit the field to add it to 82 | * @return the date with the offset added 83 | */ 84 | fun add(instant: Instant, amount: Int, dateTimeUnit: DateTimeUnit): LocalDateTime { 85 | val timezone = TimeZone.UTC 86 | val updatedInstant = instant.plus(amount, dateTimeUnit, timezone) 87 | return updatedInstant.toLocalDateTime(timezone) 88 | } 89 | 90 | /** 91 | * Gets a date for the particular date 92 | * @param year the year 93 | * @param month the month 94 | * @param day the day 95 | * @return a LocalDateTime object with a time set to 00:00:00 at utc 96 | */ 97 | private fun resolveTime(year: Int, month: Int, day: Int): LocalDateTime { 98 | return LocalDateTime(year, month, day, 0, 0, 0) 99 | } 100 | 101 | fun LocalDateTime.toUtcInstant(): Instant = toInstant(TimeZone.UTC) 102 | } 103 | -------------------------------------------------------------------------------- /Shared/Times/Makkah-UmmAlQura.json: -------------------------------------------------------------------------------- 1 | { 2 | "params": { 3 | "latitude": 21.427009, 4 | "longitude": 39.828685, 5 | "timezone": "Asia/Riyadh", 6 | "method": "UmmAlQura", 7 | "madhab": "Shafi", 8 | "highLatitudeRule": "MiddleOfTheNight" 9 | }, 10 | "source": [ 11 | "http://www.ummulqura.org.sa/Index.aspx", 12 | "adjusted +/- 1 minute", 13 | "1/5/15 - 25/3/1437 Mecca 05:39-1 06:59+1 12:26 03:32-1 05:53-1 07:23-1", 14 | "2/4/16 - 25/4/1437 Mecca 05:39 06:57 12:35 03:49-1 06:13-1 07:43-1", 15 | "3/5/16 - 25/5/1437 Mecca 05:22 06:37+1 12:33-1 03:55 06:27 07:57", 16 | "4/3/16 - 25/6/1437 Mecca 04:55 06:11+1 12:24 03:50 06:37-1 08:07-1", 17 | "5/2/16 - 25/7/1437 Mecca 04:27+1 05:48+1 12:18 03:39 06:47 08:17", 18 | "6/4/16 - 28/8/1437 Mecca 04:11 05:37+1 12:20-1 03:37-1 07:01 08:31", 19 | "7/8/16 - 3/10/1437 Mecca 04:18 05:44+1 12:26 03:43 07:07 08:37", 20 | "8/2/16 - 28/10/1437 Mecca 04:32-1 05:54 12:27 03:46 07:00-1 08:30-1", 21 | "9/4/16 - 3/12/1437 Mecca 04:48-1 06:04+1 12:20 03:45-1 06:34 08:04", 22 | "10/6/16 - 5/1/1438 Mecca 04:58-1 06:13 12:09 03:32-1 06:04 07:34", 23 | "11/5/16 - 5/2/1438 Mecca 05:09-1 06:26 12:05-1 03:19-1 05:43-1 07:13-1", 24 | "12/4/16 - 5/3/1438 Mecca 05:24-1 06:44 12:12-1 03:17 05:38 07:08" 25 | ], 26 | "times": [ 27 | { 28 | "date": "2016-01-05", 29 | "fajr": "5:38 AM", 30 | "sunrise": "7:00 AM", 31 | "dhuhr": "12:26 PM", 32 | "asr": "3:31 PM", 33 | "maghrib": "5:52 PM", 34 | "isha": "7:22 PM" 35 | }, 36 | { 37 | "date": "2016-02-04", 38 | "fajr": "5:39 AM", 39 | "sunrise": "6:57 AM", 40 | "dhuhr": "12:35 PM", 41 | "asr": "3:48 PM", 42 | "maghrib": "6:12 PM", 43 | "isha": "7:42 PM" 44 | }, 45 | { 46 | "date": "2016-03-05", 47 | "fajr": "5:22 AM", 48 | "sunrise": "6:38 AM", 49 | "dhuhr": "12:32 PM", 50 | "asr": "3:55 PM", 51 | "maghrib": "6:27 PM", 52 | "isha": "7:57 PM" 53 | }, 54 | { 55 | "date": "2016-04-03", 56 | "fajr": "4:55 AM", 57 | "sunrise": "6:12 AM", 58 | "dhuhr": "12:24 PM", 59 | "asr": "3:50 PM", 60 | "maghrib": "6:36 PM", 61 | "isha": "8:06 PM" 62 | }, 63 | { 64 | "date": "2016-05-02", 65 | "fajr": "4:28 AM", 66 | "sunrise": "5:49 AM", 67 | "dhuhr": "12:18 PM", 68 | "asr": "3:39 PM", 69 | "maghrib": "6:47 PM", 70 | "isha": "8:17 PM" 71 | }, 72 | { 73 | "date": "2016-06-04", 74 | "fajr": "4:11 AM", 75 | "sunrise": "5:38 AM", 76 | "dhuhr": "12:19 PM", 77 | "asr": "3:36 PM", 78 | "maghrib": "7:01 PM", 79 | "isha": "8:31 PM" 80 | }, 81 | { 82 | "date": "2016-07-08", 83 | "fajr": "4:18 AM", 84 | "sunrise": "5:45 AM", 85 | "dhuhr": "12:26 PM", 86 | "asr": "3:43 PM", 87 | "maghrib": "7:07 PM", 88 | "isha": "8:37 PM" 89 | }, 90 | { 91 | "date": "2016-08-02", 92 | "fajr": "4:31 AM", 93 | "sunrise": "5:54 AM", 94 | "dhuhr": "12:27 PM", 95 | "asr": "3:46 PM", 96 | "maghrib": "6:59 PM", 97 | "isha": "8:29 PM" 98 | }, 99 | { 100 | "date": "2016-09-04", 101 | "fajr": "4:47 AM", 102 | "sunrise": "6:05 AM", 103 | "dhuhr": "12:20 PM", 104 | "asr": "3:44 PM", 105 | "maghrib": "6:34 PM", 106 | "isha": "8:04 PM" 107 | }, 108 | { 109 | "date": "2016-10-06", 110 | "fajr": "4:57 AM", 111 | "sunrise": "6:13 AM", 112 | "dhuhr": "12:09 PM", 113 | "asr": "3:31 PM", 114 | "maghrib": "6:04 PM", 115 | "isha": "7:34 PM" 116 | }, 117 | { 118 | "date": "2016-11-05", 119 | "fajr": "5:08 AM", 120 | "sunrise": "6:26 AM", 121 | "dhuhr": "12:04 PM", 122 | "asr": "3:18 PM", 123 | "maghrib": "5:42 PM", 124 | "isha": "7:12 PM" 125 | }, 126 | { 127 | "date": "2016-12-04", 128 | "fajr": "5:23 AM", 129 | "sunrise": "6:44 AM", 130 | "dhuhr": "12:11 PM", 131 | "asr": "3:17 PM", 132 | "maghrib": "5:38 PM", 133 | "isha": "7:08 PM" 134 | } 135 | ] 136 | } -------------------------------------------------------------------------------- /Shared/Times/London-MoonsightingCommittee.json: -------------------------------------------------------------------------------- 1 | { 2 | "params": { 3 | "latitude": 51.507194, 4 | "longitude": -0.116711, 5 | "timezone": "Europe/London", 6 | "method": "MoonsightingCommittee", 7 | "madhab": "Hanafi", 8 | "highLatitudeRule": "MiddleOfTheNight" 9 | }, 10 | "source": [ 11 | "http://www.moonsighting.com/pray.php", 12 | "adjusted +/- 1 minute", 13 | "Jan 01 (Fri) 06:25 am 08:06 am 12:09 pm 02:15 pm 04:05 pm 05:38 pm", 14 | "Feb 01 (Mon) 06:02 am 07:40 am 12:19 pm 03:00 pm+1 04:52 pm 06:18 pm", 15 | "Mar 01 (Tue) 05:10 am 06:45 am 12:18 pm 03:48 pm+1 05:44 pm 07:03 pm", 16 | "Apr 01 (Fri) 04:59 am 06:35 am 01:09 pm 05:31 pm+1 07:37 pm 08:49 pm", 17 | "May 01 (Sun) 03:47 am 05:32 am 01:02 pm+1 06:05 pm 08:27 pm 09:32 pm", 18 | "Jun 01 (Wed) 02:55 am 04:49 am 01:03 pm 06:31 pm 09:12 pm 10:23 pm", 19 | "Jul 01 (Fri) 02:51 am 04:48 am 01:09 pm 06:41 pm-1 09:24 pm 10:38 pm", 20 | "Aug 01 (Mon) 03:38 am-1 05:25 am 01:12 pm 06:23 pm 08:51 pm 09:56 pm", 21 | "Sep 01 (Thu) 04:35 am 06:14 am 01:05 pm 05:40 pm-1 07:49 pm 08:58 pm", 22 | "Oct 01 (Sat) 05:28 am 07:02 am 12:55 pm 04:43 pm-1 06:40 pm 07:57 pm", 23 | "Nov 01 (Tue) 05:17 am+1 06:55 am 11:49 am 02:44 pm-1 04:36 pm 05:59 pm+1", 24 | "Dec 01 (Thu) 06:04 am 07:44 am 11:55 am 02:08 pm-1 03:58 pm 05:29 pm" 25 | ], 26 | "times": [ 27 | { 28 | "date": "2016-01-01", 29 | "fajr": "6:25 AM", 30 | "sunrise": "8:06 AM", 31 | "dhuhr": "12:09 PM", 32 | "asr": "2:15 PM", 33 | "maghrib": "4:05 PM", 34 | "isha": "5:38 PM" 35 | }, 36 | { 37 | "date": "2016-02-01", 38 | "fajr": "6:02 AM", 39 | "sunrise": "7:40 AM", 40 | "dhuhr": "12:19 PM", 41 | "asr": "3:01 PM", 42 | "maghrib": "4:52 PM", 43 | "isha": "6:18 PM" 44 | }, 45 | { 46 | "date": "2016-03-01", 47 | "fajr": "5:10 AM", 48 | "sunrise": "6:45 AM", 49 | "dhuhr": "12:18 PM", 50 | "asr": "3:49 PM", 51 | "maghrib": "5:44 PM", 52 | "isha": "7:03 PM" 53 | }, 54 | { 55 | "date": "2016-04-01", 56 | "fajr": "4:59 AM", 57 | "sunrise": "6:35 AM", 58 | "dhuhr": "1:09 PM", 59 | "asr": "5:32 PM", 60 | "maghrib": "7:37 PM", 61 | "isha": "8:49 PM" 62 | }, 63 | { 64 | "date": "2016-05-01", 65 | "fajr": "3:47 AM", 66 | "sunrise": "5:32 AM", 67 | "dhuhr": "1:03 PM", 68 | "asr": "6:05 PM", 69 | "maghrib": "8:27 PM", 70 | "isha": "9:32 PM" 71 | }, 72 | { 73 | "date": "2016-06-01", 74 | "fajr": "2:55 AM", 75 | "sunrise": "4:49 AM", 76 | "dhuhr": "1:03 PM", 77 | "asr": "6:31 PM", 78 | "maghrib": "9:12 PM", 79 | "isha": "10:23 PM" 80 | }, 81 | { 82 | "date": "2016-07-01", 83 | "fajr": "2:51 AM", 84 | "sunrise": "4:48 AM", 85 | "dhuhr": "1:09 PM", 86 | "asr": "6:40 PM", 87 | "maghrib": "9:24 PM", 88 | "isha": "10:38 PM" 89 | }, 90 | { 91 | "date": "2016-08-01", 92 | "fajr": "3:37 AM", 93 | "sunrise": "5:25 AM", 94 | "dhuhr": "1:12 PM", 95 | "asr": "6:23 PM", 96 | "maghrib": "8:51 PM", 97 | "isha": "9:56 PM" 98 | }, 99 | { 100 | "date": "2016-09-01", 101 | "fajr": "4:35 AM", 102 | "sunrise": "6:14 AM", 103 | "dhuhr": "1:05 PM", 104 | "asr": "5:39 PM", 105 | "maghrib": "7:49 PM", 106 | "isha": "8:58 PM" 107 | }, 108 | { 109 | "date": "2016-10-01", 110 | "fajr": "5:28 AM", 111 | "sunrise": "7:02 AM", 112 | "dhuhr": "12:55 PM", 113 | "asr": "4:42 PM", 114 | "maghrib": "6:40 PM", 115 | "isha": "7:57 PM" 116 | }, 117 | { 118 | "date": "2016-11-01", 119 | "fajr": "5:18 AM", 120 | "sunrise": "6:55 AM", 121 | "dhuhr": "11:49 AM", 122 | "asr": "2:43 PM", 123 | "maghrib": "4:36 PM", 124 | "isha": "6:00 PM" 125 | }, 126 | { 127 | "date": "2016-12-01", 128 | "fajr": "6:04 AM", 129 | "sunrise": "7:44 AM", 130 | "dhuhr": "11:55 AM", 131 | "asr": "2:07 PM", 132 | "maghrib": "3:58 PM", 133 | "isha": "5:29 PM" 134 | } 135 | ] 136 | } -------------------------------------------------------------------------------- /adhan/src/commonTest/kotlin/com/batoulapps/adhan2/internal/MathTest.kt: -------------------------------------------------------------------------------- 1 | package com.batoulapps.adhan2.internal 2 | 3 | import com.batoulapps.adhan2.data.CalendarUtil.roundedMinute 4 | import com.batoulapps.adhan2.data.TimeComponents 5 | import com.batoulapps.adhan2.internal.DoubleUtil.closestAngle 6 | import com.batoulapps.adhan2.internal.DoubleUtil.normalizeWithBound 7 | import com.batoulapps.adhan2.internal.DoubleUtil.unwindAngle 8 | import com.batoulapps.adhan2.internal.TestUtils.makeDate 9 | import kotlin.math.PI 10 | import kotlin.math.abs 11 | import kotlin.test.Test 12 | import kotlin.test.assertEquals 13 | import kotlin.test.assertNotNull 14 | import kotlin.test.assertTrue 15 | 16 | class MathTest { 17 | private fun Double.isWithin(threshold: Double) = this to threshold 18 | private fun Pair.of(value: Double) = abs(first - value) <= second 19 | private fun Pair.of(value: Int) = abs(first - value) <= second 20 | 21 | @Test 22 | fun testAngleConversion() { 23 | assertTrue((PI.toDegrees()).isWithin(0.00001).of(180.0)) 24 | assertTrue((PI / 2).toDegrees().isWithin(0.00001).of(90.0)) 25 | } 26 | 27 | @Test 28 | fun testNormalizing() { 29 | assertTrue(normalizeWithBound(2.0, -5.0).isWithin(0.00001).of(-3)) 30 | assertTrue(normalizeWithBound(-4.0, -5.0).isWithin(0.00001).of(-4)) 31 | assertTrue(normalizeWithBound(-6.0, -5.0).isWithin(0.00001).of(-1)) 32 | 33 | assertTrue(normalizeWithBound(-1.0, 24.0).isWithin(0.00001).of(23)) 34 | assertTrue(normalizeWithBound(1.0, 24.0).isWithin(0.00001).of(1)) 35 | assertTrue(normalizeWithBound(49.0, 24.0).isWithin(0.00001).of(1)) 36 | 37 | assertTrue(normalizeWithBound(361.0, 360.0).isWithin(0.00001).of(1)) 38 | assertTrue(normalizeWithBound(360.0, 360.0).isWithin(0.00001).of(0)) 39 | assertTrue(normalizeWithBound(259.0, 360.0).isWithin(0.00001).of(259)) 40 | assertTrue(normalizeWithBound(2592.0, 360.0).isWithin(0.00001).of(72)) 41 | 42 | assertTrue(unwindAngle(-45.0).isWithin(0.00001).of(315)) 43 | assertTrue(unwindAngle(361.0).isWithin(0.00001).of(1)) 44 | assertTrue(unwindAngle(360.0).isWithin(0.00001).of(0)) 45 | assertTrue(unwindAngle(259.0).isWithin(0.00001).of(259)) 46 | assertTrue(unwindAngle(2592.0).isWithin(0.00001).of(72)) 47 | 48 | assertTrue(normalizeWithBound(360.1, 360.0).isWithin(0.01).of(0.1)) 49 | } 50 | 51 | @Test 52 | fun testClosestAngle() { 53 | assertTrue(closestAngle(360.0).isWithin(0.000001).of(0)) 54 | assertTrue(closestAngle(361.0).isWithin(0.000001).of(1)) 55 | assertTrue(closestAngle(1.0).isWithin(0.000001).of(1)) 56 | assertTrue(closestAngle(-1.0).isWithin(0.000001).of(-1)) 57 | assertTrue(closestAngle(-181.0).isWithin(0.000001).of(179)) 58 | assertTrue(closestAngle(180.0).isWithin(0.000001).of(180)) 59 | assertTrue(closestAngle(359.0).isWithin(0.000001).of(-1)) 60 | assertTrue(closestAngle(-359.0).isWithin(0.000001).of(1)) 61 | assertTrue(closestAngle(1261.0).isWithin(0.000001).of(-179)) 62 | assertTrue(closestAngle(-360.1).isWithin(0.01).of(-0.1)) 63 | } 64 | 65 | @Test 66 | fun testTimeComponents() { 67 | val comps1 = TimeComponents.fromDouble(15.199) 68 | assertNotNull(comps1) 69 | assertEquals(15, comps1.hours) 70 | assertEquals(11, comps1.minutes) 71 | assertEquals(56, comps1.seconds) 72 | 73 | val comps2 = TimeComponents.fromDouble(1.0084) 74 | assertNotNull(comps2) 75 | assertEquals(1, comps2.hours) 76 | assertEquals(0, comps2.minutes) 77 | assertEquals(30, comps2.seconds) 78 | 79 | val comps3 = TimeComponents.fromDouble(1.0083) 80 | assertNotNull(comps3) 81 | assertEquals(1, comps3.hours) 82 | assertEquals(0, comps3.minutes) 83 | 84 | val comps4 = TimeComponents.fromDouble(2.1) 85 | assertNotNull(comps4) 86 | assertEquals(2, comps4.hours) 87 | assertEquals(6, comps4.minutes) 88 | 89 | val comps5 = TimeComponents.fromDouble(3.5) 90 | assertNotNull(comps5) 91 | assertEquals(3, comps5.hours) 92 | assertEquals(30, comps5.minutes) 93 | } 94 | 95 | @Test 96 | fun testMinuteRounding() { 97 | val comps1 = makeDate(year = 2015, month = 1, day = 1, hour = 10, minute = 2, second = 29) 98 | val rounded1 = roundedMinute(comps1) 99 | 100 | assertEquals(2, rounded1.minute) 101 | assertEquals(0, rounded1.second) 102 | 103 | val comps2 = makeDate(year = 2015, month = 1, day = 1, hour = 10, minute = 2, second = 31) 104 | val rounded2 = roundedMinute(comps2) 105 | 106 | assertEquals(3, rounded2.minute) 107 | assertEquals(0, rounded2.second) 108 | } 109 | } -------------------------------------------------------------------------------- /adhan/src/commonMain/kotlin/com/batoulapps/adhan2/CalculationMethod.kt: -------------------------------------------------------------------------------- 1 | package com.batoulapps.adhan2 2 | 3 | import com.batoulapps.adhan2.model.Rounding 4 | 5 | /** 6 | * Standard calculation methods for calculating prayer times 7 | */ 8 | enum class CalculationMethod { 9 | /** 10 | * Muslim World League 11 | * Uses Fajr angle of 18 and an Isha angle of 17 12 | */ 13 | MUSLIM_WORLD_LEAGUE, 14 | 15 | /** 16 | * Egyptian General Authority of Survey 17 | * Uses Fajr angle of 19.5 and an Isha angle of 17.5 18 | */ 19 | EGYPTIAN, 20 | 21 | /** 22 | * University of Islamic Sciences, Karachi 23 | * Uses Fajr angle of 18 and an Isha angle of 18 24 | */ 25 | KARACHI, 26 | 27 | /** 28 | * Umm al-Qura University, Makkah 29 | * Uses a Fajr angle of 18.5 and an Isha angle of 90. Note: You should add a +30 minute custom 30 | * adjustment of Isha during Ramadan. 31 | */ 32 | UMM_AL_QURA, 33 | 34 | /** 35 | * The Gulf Region 36 | * Uses Fajr and Isha angles of 18.2 degrees. 37 | */ 38 | DUBAI, 39 | 40 | /** 41 | * Moonsighting Committee 42 | * Uses a Fajr angle of 18 and an Isha angle of 18. Also uses seasonal adjustment values. 43 | */ 44 | MOON_SIGHTING_COMMITTEE, 45 | 46 | /** 47 | * Referred to as the ISNA method 48 | * This method is included for completeness, but is not recommended. 49 | * Uses a Fajr angle of 15 and an Isha angle of 15. 50 | */ 51 | NORTH_AMERICA, 52 | 53 | /** 54 | * Kuwait 55 | * Uses a Fajr angle of 18 and an Isha angle of 17.5 56 | */ 57 | KUWAIT, 58 | 59 | /** 60 | * Qatar 61 | * Modified version of Umm al-Qura that uses a Fajr angle of 18. 62 | */ 63 | QATAR, 64 | 65 | /** 66 | * Singapore 67 | * Uses a Fajr angle of 20 and an Isha angle of 18 68 | */ 69 | SINGAPORE, 70 | 71 | /** 72 | * Diyanet İşleri Başkanlığı, Turkey 73 | * Uses a Fajr angle of 18 and an Isha angle of 17 74 | */ 75 | TURKEY, 76 | 77 | /** 78 | * The default value for [CalculationParameters.method] when initializing a 79 | * [CalculationParameters] object. Sets a Fajr angle of 0 and an Isha angle of 0. 80 | */ 81 | OTHER; 82 | 83 | /** 84 | * Return the CalculationParameters for the given method 85 | * @return CalculationParameters for the given Calculation method 86 | */ 87 | val parameters: CalculationParameters 88 | get() = when (this) { 89 | MUSLIM_WORLD_LEAGUE -> { 90 | CalculationParameters(fajrAngle = 18.0, ishaAngle = 17.0, method = this, 91 | methodAdjustments = PrayerAdjustments(dhuhr = 1) 92 | ) 93 | } 94 | EGYPTIAN -> { 95 | CalculationParameters(fajrAngle = 19.5, ishaAngle = 17.5, method = this, 96 | methodAdjustments = PrayerAdjustments(dhuhr = 1) 97 | ) 98 | } 99 | KARACHI -> { 100 | CalculationParameters(fajrAngle = 18.0, ishaAngle = 18.0, method = this, 101 | methodAdjustments = PrayerAdjustments(dhuhr = 1) 102 | ) 103 | } 104 | UMM_AL_QURA -> { 105 | CalculationParameters(fajrAngle = 18.5, ishaInterval = 90, method = this) 106 | } 107 | DUBAI -> { 108 | CalculationParameters(fajrAngle = 18.2, ishaAngle = 18.2, method = this, 109 | methodAdjustments = PrayerAdjustments(sunrise = -3, dhuhr = 3, asr = 3, maghrib = 3) 110 | ) 111 | } 112 | MOON_SIGHTING_COMMITTEE -> { 113 | CalculationParameters(fajrAngle = 18.0, ishaAngle = 18.0, method = this, 114 | methodAdjustments = PrayerAdjustments(dhuhr = 5, maghrib = 3) 115 | ) 116 | } 117 | NORTH_AMERICA -> { 118 | CalculationParameters(fajrAngle = 15.0, ishaAngle = 15.0, method = this, 119 | methodAdjustments = PrayerAdjustments(dhuhr = 1) 120 | ) 121 | } 122 | KUWAIT -> { 123 | CalculationParameters(fajrAngle = 18.0, ishaAngle = 17.5, method = this) 124 | } 125 | QATAR -> { 126 | CalculationParameters(fajrAngle = 18.0, ishaInterval = 90, method = this) 127 | } 128 | SINGAPORE -> { 129 | CalculationParameters(fajrAngle = 20.0, ishaAngle = 18.0, method = this, 130 | methodAdjustments = PrayerAdjustments(dhuhr = 1), 131 | rounding = Rounding.UP 132 | ) 133 | } 134 | TURKEY -> { 135 | CalculationParameters(fajrAngle = 18.0, ishaAngle = 17.0, method = this, 136 | methodAdjustments = PrayerAdjustments(sunrise = -7, dhuhr = 5, asr = 4, maghrib = 7) 137 | ) 138 | } 139 | OTHER -> { 140 | CalculationParameters(fajrAngle = 0.0, ishaAngle = 0.0, method = this) 141 | } 142 | } 143 | } -------------------------------------------------------------------------------- /adhan/build.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class) 2 | 3 | import org.jetbrains.kotlin.gradle.targets.jvm.tasks.KotlinJvmTest 4 | import org.jetbrains.kotlin.gradle.targets.native.tasks.KotlinNativeTest 5 | import org.jetbrains.kotlin.gradle.targets.js.testing.KotlinJsTest 6 | import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnLockMismatchReport 7 | import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnRootExtension 8 | 9 | plugins { 10 | kotlin("multiplatform") 11 | kotlin("plugin.serialization") version "2.2.20" 12 | id("com.vanniktech.maven.publish") version "0.34.0" 13 | } 14 | 15 | group = "com.batoulapps.adhan" 16 | version = property("version") ?: "" 17 | 18 | kotlin { 19 | compilerOptions { 20 | optIn.add("kotlin.time.ExperimentalTime") 21 | } 22 | 23 | jvm() 24 | 25 | js(IR) { 26 | nodejs { 27 | testTask { 28 | useMocha { 29 | timeout = "30s" 30 | } 31 | } 32 | } 33 | } 34 | 35 | wasmJs { 36 | nodejs { 37 | testTask { 38 | useMocha { 39 | timeout = "30s" 40 | } 41 | } 42 | } 43 | } 44 | 45 | linuxX64() 46 | linuxArm64() 47 | mingwX64() 48 | 49 | iosX64() 50 | iosArm64() 51 | iosSimulatorArm64() 52 | 53 | macosArm64() 54 | macosX64() 55 | 56 | watchosX64() 57 | watchosArm32() 58 | watchosArm64() 59 | watchosSimulatorArm64() 60 | 61 | compilerOptions { 62 | freeCompilerArgs.add("-Xexpect-actual-classes") 63 | } 64 | 65 | sourceSets { 66 | val commonMain by getting { 67 | dependencies { 68 | implementation("org.jetbrains.kotlin:kotlin-stdlib") 69 | api("org.jetbrains.kotlinx:kotlinx-datetime:0.7.1") 70 | } 71 | } 72 | 73 | val commonTest by getting { 74 | dependencies { 75 | implementation(kotlin("test")) 76 | implementation(kotlin("test-common")) 77 | implementation(kotlin("test-annotations-common")) 78 | api("com.squareup.okio:okio:3.16.1") 79 | implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0") 80 | } 81 | } 82 | 83 | val jvmTest by getting { 84 | dependencies { 85 | implementation(kotlin("test-junit")) 86 | implementation("junit:junit:4.13.2") 87 | implementation(kotlin("stdlib-jdk8")) 88 | } 89 | } 90 | 91 | val jsTest by getting { 92 | dependencies { 93 | implementation(kotlin("test-js")) 94 | implementation("com.squareup.okio:okio-nodefilesystem:3.16.1") 95 | implementation(npm("@js-joda/timezone", "2.3.0")) 96 | } 97 | } 98 | 99 | val wasmJsTest by getting { 100 | dependencies { 101 | implementation(npm("@js-joda/timezone", "2.3.0")) 102 | } 103 | } 104 | } 105 | 106 | // set an environment variable and read it in the test 107 | // https://publicobject.com/2023/04/16/read-a-project-file-in-a-kotlin-multiplatform-test/ 108 | tasks.withType().configureEach { 109 | environment("ADHAN_ROOT", rootDir) 110 | } 111 | 112 | tasks.withType().configureEach { 113 | // required for the variable to propagate to the simulator 114 | environment("SIMCTL_CHILD_ADHAN_ROOT", rootDir) 115 | environment("ADHAN_ROOT", rootDir) 116 | } 117 | 118 | tasks.withType().configureEach { 119 | environment("ADHAN_ROOT", rootDir.toString()) 120 | } 121 | } 122 | 123 | mavenPublishing { 124 | publishToMavenCentral() 125 | signAllPublications() 126 | coordinates("com.batoulapps.adhan", "adhan2", version.toString()) 127 | 128 | pom { 129 | name.set("Adhan Prayertimes Library") 130 | description.set("A high precision Islamic prayer times library") 131 | url.set("https://github.com/batoulapps/adhan-kotlin") 132 | 133 | licenses { 134 | license { 135 | name.set("MIT") 136 | url.set("https://opensource.org/licenses/MIT") 137 | } 138 | } 139 | developers { 140 | developer { 141 | } 142 | } 143 | scm { 144 | url.set("https://github.com/batoulapps/adhan-kotlin") 145 | } 146 | 147 | } 148 | } 149 | 150 | // auto replace yarn.lock 151 | rootProject.plugins.withType(org.jetbrains.kotlin.gradle.targets.js.yarn.YarnPlugin::class.java) { 152 | rootProject.the().yarnLockMismatchReport = 153 | YarnLockMismatchReport.WARNING 154 | rootProject.the().yarnLockAutoReplace = true 155 | } -------------------------------------------------------------------------------- /Shared/Times/Dubai-Gulf.json: -------------------------------------------------------------------------------- 1 | { 2 | "params": { 3 | "latitude": 25.263056, 4 | "longitude": 55.297222, 5 | "timezone": "Asia/Dubai", 6 | "method": "Dubai", 7 | "madhab": "Shafi", 8 | "highLatitudeRule": "MiddleOfTheNight" 9 | }, 10 | "source": [ 11 | "https://www.awqaf.gov.ae/en/Pages/PrayerTimes.aspx", 12 | "01/01/2018 13 RABI AL-THANI 1439 5:42 AM 7:01 AM 12:25 PM 3:24 PM 5:44 PM 7:03 PM", 13 | "01/02/2018 15 JUMADA AL-AWWAL 1439 5:42 AM 6:58 AM 12:35 PM 3:44 PM 6:07 PM 7:22 PM", 14 | "01/03/2018 13 JUMADA AL-THANI 1439 5:25 AM 6:39 AM 12:34 PM 3:54 PM 6:24 PM 7:38 PM", 15 | "01/04/2018 14 RAJAB 1439 4:53 AM 6:08 AM 12:25 PM 3:54 PM 6:38 PM 7:53 PM", 16 | "01/05/2018 15 SHAABAN 1439 4:21 AM 5:40 AM 12:19 PM 3:47 PM 6:52 PM 8:12 PM", 17 | "01/06/2018 16 RAMADAN 1439 4 AM 5:25 AM 12:19 PM 3:43 PM 7:08 PM 8:33 PM", 18 | "01/07/2018 17 SHAWWAL 1439 4:02 AM 5:29 AM 12:25 PM 3:48 PM 7:16 PM 8:43 PM", 19 | "01/08/2018 19 DHU AL-QIDAH 1439 4:21 AM 5:43 AM 12:28 PM 3:55 PM 7:07 PM 8:29 PM", 20 | "01/09/2018 21 DHU AL-HIJJAH 1439 4:40 AM 5:56 AM 12:22 PM 3:51 PM 6:41 PM 7:57 PM", 21 | "01/10/2018 22 MUHARRAM 1440 4:54 AM 6:08 AM 12:11 PM 3:35 PM 6:09 PM 7:23 PM", 22 | "01/11/2018 24 SAFAR 1440 5:08 AM 6:23 AM 12:05 PM 3:17 PM 5:42 PM 6:57 PM", 23 | "01/12/2018 24 RABI AL-AWWAL 1440 5:25 AM 6:43 AM 12:11 PM 3:11 PM 5:32 PM 6:50 PM" 24 | ], 25 | "variance": 1, 26 | "times": [ 27 | { 28 | "date": "2016-01-01", 29 | "fajr": "5:42 AM", 30 | "sunrise": "7:01 AM", 31 | "dhuhr": "12:25 PM", 32 | "asr": "3:24 PM", 33 | "maghrib": "5:44 PM", 34 | "isha": "7:03 PM" 35 | }, 36 | { 37 | "date": "2016-02-01", 38 | "fajr": "5:42 AM", 39 | "sunrise": "6:58 AM", 40 | "dhuhr": "12:35 PM", 41 | "asr": "3:44 PM", 42 | "maghrib": "6:07 PM", 43 | "isha": "7:22 PM" 44 | }, 45 | { 46 | "date": "2016-03-01", 47 | "fajr": "5:25 AM", 48 | "sunrise": "6:39 AM", 49 | "dhuhr": "12:34 PM", 50 | "asr": "3:54 PM", 51 | "maghrib": "6:24 PM", 52 | "isha": "7:38 PM" 53 | }, 54 | { 55 | "date": "2016-04-01", 56 | "fajr": "4:53 AM", 57 | "sunrise": "6:08 AM", 58 | "dhuhr": "12:25 PM", 59 | "asr": "3:54 PM", 60 | "maghrib": "6:38 PM", 61 | "isha": "7:53 PM" 62 | }, 63 | { 64 | "date": "2016-05-01", 65 | "fajr": "4:21 AM", 66 | "sunrise": "5:40 AM", 67 | "dhuhr": "12:19 PM", 68 | "asr": "3:47 PM", 69 | "maghrib": "6:52 PM", 70 | "isha": "8:12 PM" 71 | }, 72 | { 73 | "date": "2016-06-01", 74 | "fajr": "4:00 AM", 75 | "sunrise": "5:25 AM", 76 | "dhuhr": "12:19 PM", 77 | "asr": "3:43 PM", 78 | "maghrib": "7:08 PM", 79 | "isha": "8:33 PM" 80 | }, 81 | { 82 | "date": "2016-07-01", 83 | "fajr": "4:02 AM", 84 | "sunrise": "5:29 AM", 85 | "dhuhr": "12:25 PM", 86 | "asr": "3:48 PM", 87 | "maghrib": "7:16 PM", 88 | "isha": "8:43 PM" 89 | }, 90 | { 91 | "date": "2016-08-01", 92 | "fajr": "4:21 AM", 93 | "sunrise": "5:43 AM", 94 | "dhuhr": "12:28 PM", 95 | "asr": "3:55 PM", 96 | "maghrib": "7:07 PM", 97 | "isha": "8:29 PM" 98 | }, 99 | { 100 | "date": "2016-09-01", 101 | "fajr": "4:40 AM", 102 | "sunrise": "5:56 AM", 103 | "dhuhr": "12:22 PM", 104 | "asr": "3:51 PM", 105 | "maghrib": "6:41 PM", 106 | "isha": "7:57 PM" 107 | }, 108 | { 109 | "date": "2016-10-01", 110 | "fajr": "4:54 AM", 111 | "sunrise": "6:08 AM", 112 | "dhuhr": "12:11 PM", 113 | "asr": "3:35 PM", 114 | "maghrib": "6:09 PM", 115 | "isha": "7:23 PM" 116 | }, 117 | { 118 | "date": "2015-11-01", 119 | "fajr": "5:08 AM", 120 | "sunrise": "6:23 AM", 121 | "dhuhr": "12:05 PM", 122 | "asr": "3:17 PM", 123 | "maghrib": "5:42 PM", 124 | "isha": "6:57 PM" 125 | }, 126 | { 127 | "date": "2015-12-01", 128 | "fajr": "5:25 AM", 129 | "sunrise": "6:43 AM", 130 | "dhuhr": "12:11 PM", 131 | "asr": "3:11 PM", 132 | "maghrib": "5:32 PM", 133 | "isha": "6:50 PM" 134 | } 135 | ] 136 | } -------------------------------------------------------------------------------- /adhan/src/commonTest/kotlin/com/batoulapps/adhan2/TimingTest.kt: -------------------------------------------------------------------------------- 1 | package com.batoulapps.adhan2 2 | 3 | import com.batoulapps.adhan2.CalculationMethod.DUBAI 4 | import com.batoulapps.adhan2.CalculationMethod.EGYPTIAN 5 | import com.batoulapps.adhan2.CalculationMethod.KARACHI 6 | import com.batoulapps.adhan2.CalculationMethod.KUWAIT 7 | import com.batoulapps.adhan2.CalculationMethod.MOON_SIGHTING_COMMITTEE 8 | import com.batoulapps.adhan2.CalculationMethod.MUSLIM_WORLD_LEAGUE 9 | import com.batoulapps.adhan2.CalculationMethod.NORTH_AMERICA 10 | import com.batoulapps.adhan2.CalculationMethod.OTHER 11 | import com.batoulapps.adhan2.CalculationMethod.QATAR 12 | import com.batoulapps.adhan2.CalculationMethod.SINGAPORE 13 | import com.batoulapps.adhan2.CalculationMethod.TURKEY 14 | import com.batoulapps.adhan2.CalculationMethod.UMM_AL_QURA 15 | import com.batoulapps.adhan2.HighLatitudeRule.MIDDLE_OF_THE_NIGHT 16 | import com.batoulapps.adhan2.HighLatitudeRule.SEVENTH_OF_THE_NIGHT 17 | import com.batoulapps.adhan2.HighLatitudeRule.TWILIGHT_ANGLE 18 | import com.batoulapps.adhan2.Madhab.HANAFI 19 | import com.batoulapps.adhan2.Madhab.SHAFI 20 | import com.batoulapps.adhan2.data.DateComponents 21 | import com.batoulapps.adhan2.data.TimingFile 22 | import com.batoulapps.adhan2.data.TimingParameters 23 | import com.batoulapps.adhan2.internal.TestUtils.getDateComponents 24 | import kotlin.math.abs 25 | import kotlin.test.Test 26 | import kotlin.test.assertTrue 27 | import kotlinx.datetime.LocalDateTime 28 | import kotlinx.datetime.TimeZone 29 | import kotlinx.datetime.toInstant 30 | import kotlinx.datetime.toLocalDateTime 31 | import kotlinx.serialization.json.Json 32 | import okio.Path.Companion.toPath 33 | import kotlin.time.Instant 34 | 35 | class TimingTest { 36 | 37 | @Test 38 | fun testTimes() { 39 | val json = Json { ignoreUnknownKeys = true } 40 | val testUtil = TestUtil() 41 | 42 | val root = (testUtil.environmentVariable("ADHAN_ROOT") ?: "").toPath() 43 | 44 | // disable time verification tests for platforms without filesystem support 45 | // currently, this is just wasm 46 | val fs = testUtil.fileSystem() ?: return 47 | 48 | val jsonPath = root.resolve("Shared/Times/") 49 | assertTrue(fs.exists(jsonPath), "Json Path Does not Exist: $jsonPath") 50 | 51 | val dir = fs.list(jsonPath) 52 | dir.forEach { path -> 53 | val byteString = fs.read(path) { 54 | readByteString() 55 | } 56 | val contents = byteString.utf8() 57 | val timingFile = json.decodeFromString(contents) 58 | validateTimingFile(timingFile) 59 | } 60 | } 61 | 62 | private fun validateTimingFile(timingFile: TimingFile) { 63 | val coordinates = Coordinates(timingFile.params.latitude, timingFile.params.longitude) 64 | val parameters = parseParameters(timingFile.params) 65 | 66 | for ((date, fajr, sunrise, dhuhr, asr, maghrib, isha) in timingFile.times) { 67 | val dateComponents: DateComponents = getDateComponents(date) 68 | val prayerTimes = PrayerTimes(coordinates, dateComponents, parameters) 69 | val fajrDifference = 70 | getDifferenceInMinutes(prayerTimes.fajr, fajr, timingFile.params.timezone) 71 | assertTrue { fajrDifference <= timingFile.variance } 72 | val sunriseDifference = 73 | getDifferenceInMinutes(prayerTimes.sunrise, sunrise, timingFile.params.timezone) 74 | assertTrue { sunriseDifference <= timingFile.variance } 75 | val dhuhrDifference = 76 | getDifferenceInMinutes(prayerTimes.dhuhr, dhuhr, timingFile.params.timezone) 77 | assertTrue { dhuhrDifference <= timingFile.variance } 78 | val asrDifference = getDifferenceInMinutes(prayerTimes.asr, asr, timingFile.params.timezone) 79 | assertTrue { asrDifference <= timingFile.variance } 80 | val maghribDifference = 81 | getDifferenceInMinutes(prayerTimes.maghrib, maghrib, timingFile.params.timezone) 82 | assertTrue { maghribDifference <= timingFile.variance } 83 | val ishaDifference = 84 | getDifferenceInMinutes(prayerTimes.isha, isha, timingFile.params.timezone) 85 | assertTrue { ishaDifference <= timingFile.variance } 86 | } 87 | } 88 | 89 | private fun getDifferenceInMinutes( 90 | prayerTime: Instant, 91 | jsonTime: String, 92 | timezone: String 93 | ): Long { 94 | val (time, amPm) = jsonTime.split(" ") 95 | val (hours, minutes) = time.split(":").map { it.toInt() } 96 | val resolvedHour = when { 97 | amPm == "PM" && hours == 12 -> 12 98 | amPm == "PM" -> hours + 12 99 | amPm == "AM" && hours == 12 -> 0 100 | else -> hours 101 | } 102 | 103 | val targetTimeZone = TimeZone.of(timezone) 104 | val calculatedDateTime = prayerTime.toLocalDateTime(targetTimeZone) 105 | 106 | val referenceDateTime = LocalDateTime( 107 | // PrayerTimes is in UTC, so we need calculatedDateTime here instead 108 | calculatedDateTime.year, 109 | calculatedDateTime.month, 110 | calculatedDateTime.day, 111 | resolvedHour, 112 | minutes 113 | ) 114 | 115 | val referenceInstant = referenceDateTime.toInstant(targetTimeZone) 116 | val calculatedInstant = calculatedDateTime.toInstant(targetTimeZone) 117 | 118 | return abs(calculatedInstant.toEpochMilliseconds() - referenceInstant.toEpochMilliseconds()) / (60 * 1000) 119 | } 120 | 121 | private fun parseParameters(timingParameters: TimingParameters): CalculationParameters { 122 | val method: CalculationMethod = when (timingParameters.method) { 123 | "MuslimWorldLeague" -> MUSLIM_WORLD_LEAGUE 124 | "Egyptian" -> EGYPTIAN 125 | "Karachi" -> KARACHI 126 | "UmmAlQura" -> UMM_AL_QURA 127 | "Dubai" -> DUBAI 128 | "MoonsightingCommittee" -> MOON_SIGHTING_COMMITTEE 129 | "NorthAmerica" -> NORTH_AMERICA 130 | "Kuwait" -> KUWAIT 131 | "Qatar" -> QATAR 132 | "Singapore" -> SINGAPORE 133 | "Turkey" -> TURKEY 134 | else -> OTHER 135 | } 136 | 137 | val parameters = method.parameters 138 | val madhab = if ("Shafi" == timingParameters.madhab) { 139 | SHAFI 140 | } else if ("Hanafi" == timingParameters.madhab) { 141 | HANAFI 142 | } else { 143 | parameters.madhab 144 | } 145 | 146 | val highLatitudeRule = if ("SeventhOfTheNight" == timingParameters.highLatitudeRule) { 147 | SEVENTH_OF_THE_NIGHT 148 | } else if ("TwilightAngle" == timingParameters.highLatitudeRule) { 149 | TWILIGHT_ANGLE 150 | } else { 151 | MIDDLE_OF_THE_NIGHT 152 | } 153 | return method.parameters.copy(madhab = madhab, highLatitudeRule = highLatitudeRule) 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Adhan Kotlin Multiplatform 2 | 3 | [![badge-travis][]][travis] [![badge-cov][]][codecov] 4 | 5 | Adhan is a well tested and well documented library for calculating Islamic prayer times. Adhan is written using Kotlin Multiplatform and works on multiple platforms. It has a small method overhead, and has no external dependencies. 6 | 7 | All astronomical calculations are high precision equations directly from the book [“Astronomical Algorithms” by Jean Meeus](http://www.willbell.com/math/mc1.htm). This book is recommended by the Astronomical Applications Department of the U.S. Naval Observatory and the Earth System Research Laboratory of the National Oceanic and Atmospheric Administration. 8 | 9 | Implementations of Adhan in other languages can be found in the parent repo [Adhan](https://github.com/batoulapps/Adhan). 10 | 11 | All primary development is for the Kotlin Multiplatform version. There is also a [branch for a pure Java version](https://github.com/batoulapps/adhan-kotlin/tree/java) of the library. 12 | 13 | ## Usage 14 | 15 | ### Gradle 16 | 17 | ``` 18 | implementation("com.batoulapps.adhan:adhan2:0.0.6") 19 | ``` 20 | 21 | **Note** - on Android, [kotlinx.datetime](https://github.com/Kotlin/kotlinx-datetime) uses `java.time`, which needs either a minimum api level of 26, or enabling of `coreLibraryDesugaring` as per the instructions [here](https://developer.android.com/studio/write/java8-support#library-desugaring). 22 | 23 | ### General Usage 24 | 25 | To get prayer times, initialize a new `PrayerTimes` object passing in coordinates, date, and calculation parameters. The fields in this are `kotlinx.datetime.Instant` in UTC that can be converted to the wanted timezone. 26 | 27 | ```kotlin 28 | val prayerTimes = PrayerTimes(coordinates, dateComponents, parameters) 29 | ``` 30 | 31 | ### Initialization parameters 32 | 33 | #### Coordinates 34 | 35 | Create a `Coordinates` object with the latitude and longitude for the location you want prayer times for. 36 | 37 | ```kotlin 38 | val coordinates = Coordinates(35.78056, -78.6389) 39 | ``` 40 | 41 | #### Date 42 | 43 | The date parameter passed in should be an instance of the `DateComponents` object. The year, month, and day values need to be populated. All other values will be ignored. The year, month and day values should be for the local date that you want prayer times for. These date values are expected to be for the Gregorian calendar. There's also a convenience method for converting a `java.util.Date` to `DateComponents`. 44 | 45 | ```kotlin 46 | val date = DateComponents(2015, 11, 1) 47 | ``` 48 | 49 | #### Calculation parameters 50 | 51 | The rest of the needed information is contained within the `CalculationParameters` class. Instead of manually initializing this class, it is recommended to use one of the pre-populated instances in the `CalculationMethod` class. You can then further customize the calculation parameters if needed. 52 | 53 | ```kotlin 54 | val params = CalculationMethod.MUSLIM_WORLD_LEAGUE.parameters 55 | .copy(madhab = Madhab.HANAFI, prayerAdjustments = PrayerAdjustments(fajr = 2)) 56 | ``` 57 | 58 | | Parameter | Description | 59 | | --------- | ----------- | 60 | | `method` | CalculationMethod name | 61 | | `fajrAngle` | Angle of the sun used to calculate Fajr | 62 | | `ishaAngle` | Angle of the sun used to calculate Isha | 63 | | `ishaInterval` | Minutes after Maghrib (if set, the time for Isha will be Maghrib plus ishaInterval) | 64 | | `madhab` | Value from the Madhab object, used to calculate Asr | 65 | | `highLatitudeRule` | Value from the HighLatitudeRule object, used to set a minimum time for Fajr and a max time for Isha | 66 | | `adjustments` | JavaScript object with custom prayer time adjustments in minutes for each prayer time | 67 | 68 | **CalculationMethod** 69 | 70 | | Value | Description | 71 | | ----- | ----------- | 72 | | `MUSLIM_WORLD_LEAGUE` | Muslim World League. Fajr angle: 18, Isha angle: 17 | 73 | | `EGYPTIAN` | Egyptian General Authority of Survey. Fajr angle: 19.5, Isha angle: 17.5 | 74 | | `KARACHI` | University of Islamic Sciences, Karachi. Fajr angle: 18, Isha angle: 18 | 75 | | `UMM_AL_QURA` | Umm al-Qura University, Makkah. Fajr angle: 18, Isha interval: 90. *Note: you should add a +30 minute custom adjustment for Isha during Ramadan.* | 76 | | `DUBAI` | Method used in UAE. Fajr and Isha angles of 18.2 degrees. | 77 | | `QATAR` | Modified version of Umm al-Qura used in Qatar. Fajr angle: 18, Isha interval: 90. | 78 | | `KUWAIT` | Method used by the country of Kuwait. Fajr angle: 18, Isha angle: 17.5 | 79 | | `MOONSIGHTING_COMMITTEE` | Moonsighting Committee. Fajr angle: 18, Isha angle: 18. Also uses seasonal adjustment values. | 80 | | `SINGAPORE` | Method used by Singapore. Fajr angle: 20, Isha angle: 18. | 81 | | `NORTH_AMERICA` | Referred to as the ISNA method. This method is included for completeness but is not recommended. Fajr angle: 15, Isha angle: 15 | 82 | | `KUWAIT` | Kuwait. Fajr angle: 18, Isha angle: 17.5 | 83 | | `OTHER` | Fajr angle: 0, Isha angle: 0. This is the default value for `method` when initializing a `CalculationParameters` object. | 84 | 85 | **Madhab** 86 | 87 | | Value | Description | 88 | | ----- | ----------- | 89 | | `SHAFI` | Earlier Asr time | 90 | | `HANAFI` | Later Asr time | 91 | 92 | **HighLatitudeRule** 93 | 94 | | Value | Description | 95 | | ----- | ----------- | 96 | | `MIDDLE_OF_THE_NIGHT` | Fajr will never be earlier than the middle of the night and Isha will never be later than the middle of the night | 97 | | `SEVENTH_OF_THE_NIGHT` | Fajr will never be earlier than the beginning of the last seventh of the night and Isha will never be later than the end of the first seventh of the night | 98 | | `TWILIGHT_ANGLE` | Similar to `SEVENTH_OF_THE_NIGHT`, but instead of 1/7, the fraction of the night used is fajrAngle/60 and ishaAngle/60 | 99 | 100 | 101 | ### Prayer Times 102 | 103 | Once the `PrayerTimes` object has been initialized it will contain values for all five prayer times and the time for sunrise. The prayer times will be Date object instances initialized with UTC values. To display these times for the local timezone, a formatting and timezone conversion formatter should be used, for example `java.text.SimpleDateFormat`. 104 | 105 | ```kotlin 106 | val formatter = SimpleDateFormat("hh:mm a") 107 | formatter.setTimeZone(TimeZone.getTimeZone("America/New_York")) 108 | formatter.format(Date(prayerTimes.fajr.toEpochMilliseconds())) 109 | ``` 110 | 111 | ### Qibla 112 | 113 | As of version 1.1.0, this library provides a `Qibla` class for getting the qibla for a given location. 114 | 115 | ```kotlin 116 | val coordinates = Coordinates(latitude, longitude) 117 | val qibla = Qibla(coordinates) 118 | // qibla.direction is the qibla direction 119 | ``` 120 | 121 | ### SunnahTimes 122 | 123 | The library provides a `SunnahTimes` class. 124 | 125 | ```kotlin 126 | val sunnahTimes = SunnahTimes(prayerTimes) 127 | // sunnahTimes.middleOfTheNight is the midpoint between Maghrib and Fajr 128 | // sunnahTimes.lastThirdOfTheNight is the last third between Maghrib and Fajr 129 | ``` 130 | 131 | ## Full Example 132 | 133 | See an example in the `samples` module. 134 | 135 | [badge-travis]: https://travis-ci.org/batoulapps/adhan-java.svg?branch=master 136 | [badge-cov]: https://codecov.io/gh/batoulapps/adhan-java/branch/master/graph/badge.svg 137 | [travis]: https://travis-ci.org/batoulapps/adhan-java 138 | [codecov]: https://codecov.io/gh/batoulapps/adhan-java 139 | -------------------------------------------------------------------------------- /adhan/src/commonTest/kotlin/com/batoulapps/adhan2/SunnahTimesTest.kt: -------------------------------------------------------------------------------- 1 | package com.batoulapps.adhan2 2 | 3 | import com.batoulapps.adhan2.CalculationMethod.MOON_SIGHTING_COMMITTEE 4 | import com.batoulapps.adhan2.CalculationMethod.MUSLIM_WORLD_LEAGUE 5 | import com.batoulapps.adhan2.CalculationMethod.NORTH_AMERICA 6 | import com.batoulapps.adhan2.HighLatitudeRule.MIDDLE_OF_THE_NIGHT 7 | import com.batoulapps.adhan2.HighLatitudeRule.SEVENTH_OF_THE_NIGHT 8 | import com.batoulapps.adhan2.data.DateComponents 9 | import com.batoulapps.adhan2.internal.TestUtils.pad 10 | import kotlinx.datetime.TimeZone 11 | import kotlinx.datetime.number 12 | import kotlinx.datetime.toLocalDateTime 13 | import kotlin.test.Test 14 | import kotlin.test.assertEquals 15 | import kotlin.time.Instant 16 | 17 | class SunnahTimesTest { 18 | 19 | @Test 20 | fun testSunnahTimesNY() { 21 | val params = NORTH_AMERICA.parameters 22 | val coordinates = Coordinates(35.7750, -78.6336) 23 | 24 | val zoneId = "America/New_York" 25 | val todayComponents = DateComponents(2015, 7, 12) 26 | val todayPrayers = PrayerTimes(coordinates, todayComponents, params) 27 | assertEquals("7/12/15, 8:32 PM", stringifyAtTimezone(todayPrayers.maghrib, zoneId)) 28 | 29 | val tomorrowComponents = DateComponents(2015, 7, 13) 30 | val tomorrowPrayers = PrayerTimes(coordinates, tomorrowComponents, params) 31 | assertEquals("7/13/15, 4:43 AM", stringifyAtTimezone(tomorrowPrayers.fajr, zoneId)) 32 | 33 | /* 34 | Night: 8:32 PM to 4:43 AM 35 | Duration: 8 hours, 11 minutes 36 | Middle = 8:32 PM + 4 hours, 5.5 minutes = 12:37:30 AM which rounds to 12:38 AM 37 | Last Third = 8:32 PM + 5 hours, 27.3 minutes = 1:59:20 AM which rounds to 1:59 AM 38 | */ 39 | val sunnahTimes = SunnahTimes(todayPrayers) 40 | assertEquals("7/13/15, 12:38 AM", stringifyAtTimezone(sunnahTimes.middleOfTheNight, zoneId)) 41 | assertEquals("7/13/15, 1:59 AM", stringifyAtTimezone(sunnahTimes.lastThirdOfTheNight, zoneId)) 42 | } 43 | 44 | @Test 45 | fun testSunnahTimesLondon() { 46 | val params = MOON_SIGHTING_COMMITTEE.parameters 47 | val coordinates = Coordinates(51.5074, -0.1278) 48 | 49 | val zoneId = "Europe/London" 50 | val todayComponents = DateComponents(2016, 12, 31) 51 | val todayPrayers = PrayerTimes(coordinates, todayComponents, params) 52 | assertEquals("12/31/16, 4:04 PM", stringifyAtTimezone(todayPrayers.maghrib, zoneId)) 53 | 54 | val tomorrowComponents = DateComponents(2017, 1, 1) 55 | val tomorrowPrayers = PrayerTimes(coordinates, tomorrowComponents, params) 56 | assertEquals("1/1/17, 6:25 AM", stringifyAtTimezone(tomorrowPrayers.fajr, zoneId)) 57 | 58 | /* 59 | Night: 4:04 PM to 6:25 AM 60 | Duration: 14 hours, 21 minutes 61 | Middle = 4:04 PM + 7 hours, 10.5 minutes = 11:14:30 PM which rounds to 11:15 PM 62 | Last Third = 4:04 PM + 9 hours, 34 minutes = 1:38 AM 63 | */ 64 | val sunnahTimes = SunnahTimes(todayPrayers) 65 | assertEquals("12/31/16, 11:15 PM", stringifyAtTimezone(sunnahTimes.middleOfTheNight, zoneId)) 66 | assertEquals("1/1/17, 1:38 AM", stringifyAtTimezone(sunnahTimes.lastThirdOfTheNight, zoneId)) 67 | } 68 | 69 | @Test 70 | fun testSunnahTimesOslo() { 71 | val params = MUSLIM_WORLD_LEAGUE.parameters.copy(highLatitudeRule = MIDDLE_OF_THE_NIGHT) 72 | val coordinates = Coordinates(59.9094, 10.7349) 73 | 74 | val zoneId = "Europe/Oslo" 75 | val todayComponents = DateComponents(2016, 7, 1) 76 | val todayPrayers = PrayerTimes(coordinates, todayComponents, params) 77 | assertEquals("7/1/16, 10:41 PM", stringifyAtTimezone(todayPrayers.maghrib, zoneId)) 78 | 79 | val tomorrowComponents = DateComponents(2016, 7, 2) 80 | val tomorrowPrayers = PrayerTimes(coordinates, tomorrowComponents, params) 81 | assertEquals("7/2/16, 1:20 AM", stringifyAtTimezone(tomorrowPrayers.fajr, zoneId)) 82 | 83 | /* 84 | Night: 10:41 PM to 1:20 AM 85 | Duration: 2 hours, 39 minutes 86 | Middle = 10:41 PM + 1 hours, 19.5 minutes = 12:00:30 AM which rounds to 12:01 AM 87 | Last Third = 10:41 PM + 1 hours, 46 minutes = 12:27 AM 88 | */ 89 | val sunnahTimes = SunnahTimes(todayPrayers) 90 | assertEquals("7/2/16, 12:01 AM", stringifyAtTimezone(sunnahTimes.middleOfTheNight, zoneId)) 91 | assertEquals("7/2/16, 12:27 AM", stringifyAtTimezone(sunnahTimes.lastThirdOfTheNight, zoneId)) 92 | } 93 | 94 | @Test 95 | fun testSunnahTimesDST1() { 96 | val params = NORTH_AMERICA.parameters 97 | val coordinates = Coordinates(37.7749, -122.4194) 98 | 99 | val zoneId = "America/Los_Angeles" 100 | val todayComponents = DateComponents(2017, 3, 11) 101 | val todayPrayers = PrayerTimes(coordinates, todayComponents, params) 102 | assertEquals("3/11/17, 5:14 AM", stringifyAtTimezone(todayPrayers.fajr, zoneId)) 103 | assertEquals("3/11/17, 6:13 PM", stringifyAtTimezone(todayPrayers.maghrib, zoneId)) 104 | 105 | val tomorrowComponents = DateComponents(2017, 3, 12) 106 | val tomorrowPrayers = PrayerTimes(coordinates, tomorrowComponents, params) 107 | assertEquals("3/12/17, 6:13 AM", stringifyAtTimezone(tomorrowPrayers.fajr, zoneId)) 108 | assertEquals("3/12/17, 7:14 PM", stringifyAtTimezone(tomorrowPrayers.maghrib, zoneId)) 109 | 110 | /* 111 | Night: 6:13 PM PST to 6:13 AM PDT 112 | Duration: 11 hours (1 hour is skipped due to DST) 113 | Middle = 6:13 PM + 5 hours, 30 minutes = 11:43 PM 114 | Last Third = 6:13 PM + 7 hours, 20 minutes = 1:33 AM 115 | */ 116 | val sunnahTimes = SunnahTimes(todayPrayers) 117 | assertEquals("3/11/17, 11:43 PM", stringifyAtTimezone(sunnahTimes.middleOfTheNight, zoneId)) 118 | assertEquals("3/12/17, 1:33 AM", stringifyAtTimezone(sunnahTimes.lastThirdOfTheNight, zoneId)) 119 | } 120 | 121 | @Test 122 | fun testSunnahTimesDST2() { 123 | val params = MUSLIM_WORLD_LEAGUE.parameters.copy(highLatitudeRule = SEVENTH_OF_THE_NIGHT) 124 | val coordinates = Coordinates(48.8566, 2.3522) 125 | 126 | val zoneId = "Europe/Paris" 127 | val todayComponents = DateComponents(2015, 10, 24) 128 | val todayPrayers = PrayerTimes(coordinates, todayComponents, params) 129 | assertEquals("10/24/15, 6:38 AM", stringifyAtTimezone(todayPrayers.fajr, zoneId)) 130 | assertEquals("10/24/15, 6:45 PM", stringifyAtTimezone(todayPrayers.maghrib, zoneId)) 131 | 132 | val tomorrowComponents = DateComponents(2015, 10, 25) 133 | val tomorrowPrayers = PrayerTimes(coordinates, tomorrowComponents, params) 134 | assertEquals("10/25/15, 5:40 AM", stringifyAtTimezone(tomorrowPrayers.fajr, zoneId)) 135 | assertEquals("10/25/15, 5:43 PM", stringifyAtTimezone(tomorrowPrayers.maghrib, zoneId)) 136 | 137 | /* 138 | Night: 6:45 PM CEST to 5:40 AM CET 139 | Duration: 11 hours 55 minutes (1 extra hour is added due to DST) 140 | Middle = 6:45 PM + 5 hours, 57.5 minutes = 12:42:30 AM which rounds to 12:43 AM 141 | Last Third = 6:45 PM + 7 hours, 56 minutes, 40 seconds = 2:41:40 AM which rounds to 2:42 AM 142 | */ 143 | val sunnahTimes = SunnahTimes(todayPrayers) 144 | assertEquals("10/25/15, 12:43 AM", stringifyAtTimezone(sunnahTimes.middleOfTheNight, zoneId)) 145 | assertEquals("10/25/15, 2:42 AM", stringifyAtTimezone(sunnahTimes.lastThirdOfTheNight, zoneId)) 146 | } 147 | 148 | private fun stringifyAtTimezone(time: Instant, zoneId: String): String { 149 | val timeZone = TimeZone.of(zoneId) 150 | val localDateTime = time.toLocalDateTime(timeZone) 151 | 152 | // hour is 0-23 153 | val initialHour = localDateTime.hour 154 | val hour = when { 155 | initialHour == 0 -> 12 156 | initialHour > 12 -> initialHour - 12 157 | else -> initialHour 158 | } 159 | val minutes = pad(localDateTime.minute) 160 | val amPM = if (localDateTime.hour >= 12) "PM" else "AM" 161 | val year = pad(localDateTime.year % 2000) 162 | return "${localDateTime.month.number}/${localDateTime.day}/$year, $hour:$minutes $amPM" 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /Shared/Times/Ankara-Turkey.json: -------------------------------------------------------------------------------- 1 | { 2 | "params": { 3 | "latitude": 39.939382, 4 | "longitude": 32.819713, 5 | "timezone": "Europe/Istanbul", 6 | "method": "Turkey", 7 | "madhab": "Shafi", 8 | "highLatitudeRule": "MiddleOfTheNight" 9 | }, 10 | "source": [ 11 | "https://www.yenisafak.com/en/ankara-prayer-times-01.01.2019", 12 | "2019-01-01 06:33 08:03 12:57 15:20 17:40 19:05", 13 | "2019-02-01 06:25 07:51 13:07 15:49 18:14 19:34", 14 | "2019-03-01 05:53 07:16 13:06 16:14 18:46 20:04", 15 | "2019-04-01 05:01 06:27 12:58 16:31 19:19 20:39", 16 | "2019-05-01 04:07 05:43 12:51 16:40 19:49 21:18", 17 | "2019-06-01 03:24 05:15 12:51 16:48 20:17 22:00", 18 | "2019-07-01 03:21 05:16 12:57 16:55 20:28 22:15", 19 | "2019-08-01 03:58 05:40 13:00 16:53 20:10 21:45", 20 | "2019-09-01 04:40 06:09 12:54 16:34 19:29 20:52", 21 | "2019-10-01 05:14 06:37 12:44 16:02 18:40 19:58", 22 | "2019-11-01 05:45 07:10 12:37 15:28 17:55 19:14", 23 | "2019-12-01 06:14 07:43 12:42 15:10 17:31 18:55" 24 | ], 25 | "variance": 2, 26 | "times": [ 27 | { 28 | "date": "2019-01-01", 29 | "fajr": "6:33 AM", 30 | "sunrise": "8:03 AM", 31 | "dhuhr": "12:57 PM", 32 | "asr": "3:20 PM", 33 | "maghrib": "5:40 PM", 34 | "isha": "7:05 PM" 35 | }, 36 | { 37 | "date": "2019-01-15", 38 | "fajr": "6:33 AM", 39 | "sunrise": "8:02 AM", 40 | "dhuhr": "1:03 PM", 41 | "asr": "3:32 PM", 42 | "maghrib": "5:54 PM", 43 | "isha": "7:17 PM" 44 | }, 45 | { 46 | "date": "2019-02-01", 47 | "fajr": "6:25 AM", 48 | "sunrise": "7:51 AM", 49 | "dhuhr": "1:07 PM", 50 | "asr": "3:49 PM", 51 | "maghrib": "6:14 PM", 52 | "isha": "7:34 PM" 53 | }, 54 | { 55 | "date": "2019-02-15", 56 | "fajr": "6:11 AM", 57 | "sunrise": "7:35 AM", 58 | "dhuhr": "1:08 PM", 59 | "asr": "4:02 PM", 60 | "maghrib": "6:30 PM", 61 | "isha": "7:49 PM" 62 | }, 63 | { 64 | "date": "2019-03-01", 65 | "fajr": "5:53 AM", 66 | "sunrise": "7:16 AM", 67 | "dhuhr": "1:06 PM", 68 | "asr": "4:14 PM", 69 | "maghrib": "6:46 PM", 70 | "isha": "8:04 PM" 71 | }, 72 | { 73 | "date": "2019-03-15", 74 | "fajr": "5:31 AM", 75 | "sunrise": "6:54 AM", 76 | "dhuhr": "1:03 PM", 77 | "asr": "4:23 PM", 78 | "maghrib": "7:01 PM", 79 | "isha": "8:19 PM" 80 | }, 81 | { 82 | "date": "2019-04-01", 83 | "fajr": "5:01 AM", 84 | "sunrise": "6:27 AM", 85 | "dhuhr": "12:58 PM", 86 | "asr": "4:31 PM", 87 | "maghrib": "7:19 PM", 88 | "isha": "8:39 PM" 89 | }, 90 | { 91 | "date": "2019-04-15", 92 | "fajr": "4:35 AM", 93 | "sunrise": "6:05 AM", 94 | "dhuhr": "12:54 PM", 95 | "asr": "4:35 PM", 96 | "maghrib": "7:33 PM", 97 | "isha": "8:56 PM" 98 | }, 99 | { 100 | "date": "2019-05-01", 101 | "fajr": "4:07 AM", 102 | "sunrise": "5:43 AM", 103 | "dhuhr": "12:51 PM", 104 | "asr": "4:40 PM", 105 | "maghrib": "7:49 PM", 106 | "isha": "9:18 PM" 107 | }, 108 | { 109 | "date": "2019-05-15", 110 | "fajr": "3:44 AM", 111 | "sunrise": "5:27 AM", 112 | "dhuhr": "12:50 PM", 113 | "asr": "4:43 PM", 114 | "maghrib": "8:03 PM", 115 | "isha": "9:39 PM" 116 | }, 117 | { 118 | "date": "2019-06-01", 119 | "fajr": "3:24 AM", 120 | "sunrise": "5:15 AM", 121 | "dhuhr": "12:51 PM", 122 | "asr": "4:48 PM", 123 | "maghrib": "8:17 PM", 124 | "isha": "10:00 PM" 125 | }, 126 | { 127 | "date": "2019-06-15", 128 | "fajr": "3:17 AM", 129 | "sunrise": "5:12 AM", 130 | "dhuhr": "12:54 PM", 131 | "asr": "4:51 PM", 132 | "maghrib": "8:26 PM", 133 | "isha": "10:12 PM" 134 | }, 135 | { 136 | "date": "2019-07-01", 137 | "fajr": "3:21 AM", 138 | "sunrise": "5:16 AM", 139 | "dhuhr": "12:57 PM", 140 | "asr": "4:55 PM", 141 | "maghrib": "8:28 PM", 142 | "isha": "10:15 PM" 143 | }, 144 | { 145 | "date": "2019-07-15", 146 | "fajr": "3:35 AM", 147 | "sunrise": "5:25 AM", 148 | "dhuhr": "1:00 PM", 149 | "asr": "4:56 PM", 150 | "maghrib": "8:24 PM", 151 | "isha": "10:06 PM" 152 | }, 153 | { 154 | "date": "2019-08-01", 155 | "fajr": "3:58 AM", 156 | "sunrise": "5:40 AM", 157 | "dhuhr": "1:00 PM", 158 | "asr": "4:53 PM", 159 | "maghrib": "8:10 PM", 160 | "isha": "9:45 PM" 161 | }, 162 | { 163 | "date": "2019-08-15", 164 | "fajr": "4:18 AM", 165 | "sunrise": "5:53 AM", 166 | "dhuhr": "12:58 PM", 167 | "asr": "4:46 PM", 168 | "maghrib": "7:54 PM", 169 | "isha": "9:22 PM" 170 | }, 171 | { 172 | "date": "2019-09-01", 173 | "fajr": "4:40 AM", 174 | "sunrise": "6:09 AM", 175 | "dhuhr": "12:54 PM", 176 | "asr": "4:34 PM", 177 | "maghrib": "7:29 PM", 178 | "isha": "8:52 PM" 179 | }, 180 | { 181 | "date": "2019-09-15", 182 | "fajr": "4:57 AM", 183 | "sunrise": "6:22 AM", 184 | "dhuhr": "12:49 PM", 185 | "asr": "4:20 PM", 186 | "maghrib": "7:06 PM", 187 | "isha": "8:26 PM" 188 | }, 189 | { 190 | "date": "2019-10-01", 191 | "fajr": "5:14 AM", 192 | "sunrise": "6:37 AM", 193 | "dhuhr": "12:44 PM", 194 | "asr": "4:02 PM", 195 | "maghrib": "6:40 PM", 196 | "isha": "7:58 PM" 197 | }, 198 | { 199 | "date": "2019-10-15", 200 | "fajr": "5:28 AM", 201 | "sunrise": "6:51 AM", 202 | "dhuhr": "12:40 PM", 203 | "asr": "3:46 PM", 204 | "maghrib": "6:18 PM", 205 | "isha": "7:36 PM" 206 | }, 207 | { 208 | "date": "2019-11-01", 209 | "fajr": "5:45 AM", 210 | "sunrise": "7:10 AM", 211 | "dhuhr": "12:37 PM", 212 | "asr": "3:28 PM", 213 | "maghrib": "5:55 PM", 214 | "isha": "7:14 PM" 215 | }, 216 | { 217 | "date": "2019-11-15", 218 | "fajr": "5:59 AM", 219 | "sunrise": "7:26 AM", 220 | "dhuhr": "12:38 PM", 221 | "asr": "3:16 PM", 222 | "maghrib": "5:40 PM", 223 | "isha": "7:02 PM" 224 | }, 225 | { 226 | "date": "2019-12-01", 227 | "fajr": "6:14 AM", 228 | "sunrise": "7:43 AM", 229 | "dhuhr": "12:42 PM", 230 | "asr": "3:10 PM", 231 | "maghrib": "5:31 PM", 232 | "isha": "6:55 PM" 233 | }, 234 | { 235 | "date": "2019-12-15", 236 | "fajr": "6:25 AM", 237 | "sunrise": "7:56 AM", 238 | "dhuhr": "12:48 PM", 239 | "asr": "3:10 PM", 240 | "maghrib": "5:31 PM", 241 | "isha": "6:56 PM" 242 | } 243 | ] 244 | } -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit 88 | 89 | # Use the maximum available, or set MAX_FD != -1 to use that value. 90 | MAX_FD=maximum 91 | 92 | warn () { 93 | echo "$*" 94 | } >&2 95 | 96 | die () { 97 | echo 98 | echo "$*" 99 | echo 100 | exit 1 101 | } >&2 102 | 103 | # OS specific support (must be 'true' or 'false'). 104 | cygwin=false 105 | msys=false 106 | darwin=false 107 | nonstop=false 108 | case "$( uname )" in #( 109 | CYGWIN* ) cygwin=true ;; #( 110 | Darwin* ) darwin=true ;; #( 111 | MSYS* | MINGW* ) msys=true ;; #( 112 | NONSTOP* ) nonstop=true ;; 113 | esac 114 | 115 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 116 | 117 | 118 | # Determine the Java command to use to start the JVM. 119 | if [ -n "$JAVA_HOME" ] ; then 120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 121 | # IBM's JDK on AIX uses strange locations for the executables 122 | JAVACMD=$JAVA_HOME/jre/sh/java 123 | else 124 | JAVACMD=$JAVA_HOME/bin/java 125 | fi 126 | if [ ! -x "$JAVACMD" ] ; then 127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 128 | 129 | Please set the JAVA_HOME variable in your environment to match the 130 | location of your Java installation." 131 | fi 132 | else 133 | JAVACMD=java 134 | if ! command -v java >/dev/null 2>&1 135 | then 136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | fi 142 | 143 | # Increase the maximum file descriptors if we can. 144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 145 | case $MAX_FD in #( 146 | max*) 147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 148 | # shellcheck disable=SC2039,SC3045 149 | MAX_FD=$( ulimit -H -n ) || 150 | warn "Could not query maximum file descriptor limit" 151 | esac 152 | case $MAX_FD in #( 153 | '' | soft) :;; #( 154 | *) 155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 156 | # shellcheck disable=SC2039,SC3045 157 | ulimit -n "$MAX_FD" || 158 | warn "Could not set maximum file descriptor limit to $MAX_FD" 159 | esac 160 | fi 161 | 162 | # Collect all arguments for the java command, stacking in reverse order: 163 | # * args from the command line 164 | # * the main class name 165 | # * -classpath 166 | # * -D...appname settings 167 | # * --module-path (only if needed) 168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 169 | 170 | # For Cygwin or MSYS, switch paths to Windows format before running java 171 | if "$cygwin" || "$msys" ; then 172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command: 206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 207 | # and any embedded shellness will be escaped. 208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 209 | # treated as '${Hostname}' itself on the command line. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -classpath "$CLASSPATH" \ 214 | org.gradle.wrapper.GradleWrapperMain \ 215 | "$@" 216 | 217 | # Stop when "xargs" is not available. 218 | if ! command -v xargs >/dev/null 2>&1 219 | then 220 | die "xargs is not available" 221 | fi 222 | 223 | # Use "xargs" to parse quoted args. 224 | # 225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 226 | # 227 | # In Bash we could simply go: 228 | # 229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 230 | # set -- "${ARGS[@]}" "$@" 231 | # 232 | # but POSIX shell has neither arrays nor command substitution, so instead we 233 | # post-process each arg (as a line of input to sed) to backslash-escape any 234 | # character that might be a shell metacharacter, then use eval to reverse 235 | # that process (while maintaining the separation between arguments), and wrap 236 | # the whole thing up as a single "set" statement. 237 | # 238 | # This will of course break if any of these variables contains a newline or 239 | # an unmatched quote. 240 | # 241 | 242 | eval "set -- $( 243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 244 | xargs -n1 | 245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 246 | tr '\n' ' ' 247 | )" '"$@"' 248 | 249 | exec "$JAVACMD" "$@" 250 | -------------------------------------------------------------------------------- /adhan/src/commonMain/kotlin/com/batoulapps/adhan2/internal/Astronomical.kt: -------------------------------------------------------------------------------- 1 | package com.batoulapps.adhan2.internal 2 | 3 | import com.batoulapps.adhan2.Coordinates 4 | import com.batoulapps.adhan2.internal.DoubleUtil.closestAngle 5 | import com.batoulapps.adhan2.internal.DoubleUtil.normalizeWithBound 6 | import com.batoulapps.adhan2.internal.DoubleUtil.unwindAngle 7 | import kotlin.math.acos 8 | import kotlin.math.asin 9 | import kotlin.math.cos 10 | import kotlin.math.pow 11 | import kotlin.math.sin 12 | 13 | /** 14 | * Astronomical equations 15 | */ 16 | internal object Astronomical { 17 | /** 18 | * The geometric mean longitude of the sun in degrees. 19 | * @param T the julian century 20 | * @return the geometric longitude of the sun in degrees 21 | */ 22 | fun meanSolarLongitude(T: Double): Double { 23 | /* Equation from Astronomical Algorithms page 163 */ 24 | val term1 = 280.4664567 25 | val term2 = 36000.76983 * T 26 | val term3: Double = 0.0003032 * T.pow(2.0) 27 | val L0 = term1 + term2 + term3 28 | return unwindAngle(L0) 29 | } 30 | 31 | /** 32 | * The geometric mean longitude of the moon in degrees 33 | * @param T the julian century 34 | * @return the geometric mean longitude of the moon in degrees 35 | */ 36 | fun meanLunarLongitude(T: Double): Double { 37 | /* Equation from Astronomical Algorithms page 144 */ 38 | val term1 = 218.3165 39 | val term2 = 481267.8813 * T 40 | val Lp = term1 + term2 41 | return unwindAngle(Lp) 42 | } 43 | 44 | /** 45 | * The apparent longitude of the Sun, referred to the true equinox of the date. 46 | * @param T the julian century 47 | * @param L0 the mean longitude 48 | * @return the true equinox of the date 49 | */ 50 | fun apparentSolarLongitude(T: Double, L0: Double): Double { 51 | /* Equation from Astronomical Algorithms page 164 */ 52 | val longitude = L0 + solarEquationOfTheCenter(T, meanSolarAnomaly(T)) 53 | val Ω = 125.04 - 1934.136 * T 54 | val λ: Double = longitude - 0.00569 - 0.00478 * sin(Ω.toRadians()) 55 | return unwindAngle(λ) 56 | } 57 | 58 | /** 59 | * The ascending lunar node longitude 60 | * @param T the julian century 61 | * @return the ascending lunar node longitude 62 | */ 63 | fun ascendingLunarNodeLongitude(T: Double): Double { 64 | /* Equation from Astronomical Algorithms page 144 */ 65 | val term1 = 125.04452 66 | val term2 = 1934.136261 * T 67 | val term3: Double = 0.0020708 * T.pow(2.0) 68 | val term4: Double = T.pow(3.0) / 450000 69 | val Ω = term1 - term2 + term3 + term4 70 | return unwindAngle(Ω) 71 | } 72 | 73 | /** 74 | * The mean anomaly of the sun 75 | * @param T the julian century 76 | * @return the mean solar anomaly 77 | */ 78 | fun meanSolarAnomaly(T: Double): Double { 79 | /* Equation from Astronomical Algorithms page 163 */ 80 | val term1 = 357.52911 81 | val term2 = 35999.05029 * T 82 | val term3: Double = 0.0001537 * T.pow(2.0) 83 | val M = term1 + term2 - term3 84 | return unwindAngle(M) 85 | } 86 | 87 | /** 88 | * The Sun's equation of the center in degrees. 89 | * @param T the julian century 90 | * @param M the mean anomaly 91 | * @return the sun's equation of the center in degrees 92 | */ 93 | fun solarEquationOfTheCenter(T: Double, M: Double): Double { 94 | /* Equation from Astronomical Algorithms page 164 */ 95 | val Mrad: Double = M.toRadians() 96 | val term1: Double = 97 | (1.914602 - 0.004817 * T - 0.000014 * T.pow(2.0)) * sin(Mrad) 98 | val term2: Double = (0.019993 - 0.000101 * T) * sin(2 * Mrad) 99 | val term3: Double = 0.000289 * sin(3 * Mrad) 100 | return term1 + term2 + term3 101 | } 102 | 103 | /** 104 | * The mean obliquity of the ecliptic in degrees 105 | * formula adopted by the International Astronomical Union. 106 | * @param T the julian century 107 | * @return the mean obliquity of the ecliptic in degrees 108 | */ 109 | fun meanObliquityOfTheEcliptic(T: Double): Double { 110 | /* Equation from Astronomical Algorithms page 147 */ 111 | val term1 = 23.439291 112 | val term2 = 0.013004167 * T 113 | val term3: Double = 0.0000001639 * T.pow(2.0) 114 | val term4: Double = 0.0000005036 * T.pow(3.0) 115 | return term1 - term2 - term3 + term4 116 | } 117 | 118 | /** 119 | * The mean obliquity of the ecliptic, corrected for calculating the 120 | * apparent position of the sun, in degrees. 121 | * @param T the julian century 122 | * @param ε0 the mean obliquity of the ecliptic 123 | * @return the corrected mean obliquity of the ecliptic in degrees 124 | */ 125 | fun apparentObliquityOfTheEcliptic(T: Double, ε0: Double): Double { 126 | /* Equation from Astronomical Algorithms page 165 */ 127 | val O = 125.04 - 1934.136 * T 128 | return ε0 + 0.00256 * cos(O.toRadians()) 129 | } 130 | 131 | /** 132 | * Mean sidereal time, the hour angle of the vernal equinox, in degrees. 133 | * @param T the julian century 134 | * @return the mean sidereal time in degrees 135 | */ 136 | fun meanSiderealTime(T: Double): Double { 137 | /* Equation from Astronomical Algorithms page 165 */ 138 | val JD = T * 36525 + 2451545.0 139 | val term1 = 280.46061837 140 | val term2 = 360.98564736629 * (JD - 2451545) 141 | val term3: Double = 0.000387933 * T.pow(2.0) 142 | val term4: Double = T.pow(3.0) / 38710000 143 | val θ = term1 + term2 + term3 - term4 144 | return unwindAngle(θ) 145 | } 146 | 147 | /** 148 | * Get the nutation in longitude 149 | * @param T the julian century 150 | * @param L0 the solar longitude 151 | * @param Lp the lunar longitude 152 | * @param Ω the ascending node 153 | * @return the nutation in longitude 154 | */ 155 | fun nutationInLongitude(T: Double, L0: Double, Lp: Double, Ω: Double): Double { 156 | /* Equation from Astronomical Algorithms page 144 */ 157 | val term1: Double = -17.2 / 3600 * sin(Ω.toRadians()) 158 | val term2: Double = 1.32 / 3600 * sin(2 * L0.toRadians()) 159 | val term3: Double = 0.23 / 3600 * sin(2 * Lp.toRadians()) 160 | val term4: Double = 0.21 / 3600 * sin(2 * Ω.toRadians()) 161 | return term1 - term2 - term3 + term4 162 | } 163 | 164 | /** 165 | * Get the nutation in obliquity 166 | * @param T the julian century 167 | * @param L0 the solar longitude 168 | * @param Lp the lunar longitude 169 | * @param Ω the ascending node 170 | * @return the nutation in obliquity 171 | */ 172 | fun nutationInObliquity(T: Double, L0: Double, Lp: Double, Ω: Double): Double { 173 | /* Equation from Astronomical Algorithms page 144 */ 174 | val term1: Double = 9.2 / 3600 * cos(Ω.toRadians()) 175 | val term2: Double = 0.57 / 3600 * cos(2 * L0.toRadians()) 176 | val term3: Double = 0.10 / 3600 * cos(2 * Lp.toRadians()) 177 | val term4: Double = 0.09 / 3600 * cos(2 * Ω.toRadians()) 178 | return term1 + term2 + term3 - term4 179 | } 180 | 181 | /** 182 | * Return the altitude of the celestial body 183 | * @param φ the observer latitude 184 | * @param δ the declination 185 | * @param H the local hour angle 186 | * @return the altitude of the celestial body 187 | */ 188 | fun altitudeOfCelestialBody(φ: Double, δ: Double, H: Double): Double { 189 | /* Equation from Astronomical Algorithms page 93 */ 190 | val term1: Double = sin(φ.toRadians()) * sin(δ.toRadians()) 191 | val term2: Double = cos(φ.toRadians()) * cos(δ.toRadians()) * cos(H.toRadians()) 192 | return asin(term1 + term2).toDegrees() 193 | } 194 | 195 | /** 196 | * Return the approximate transite 197 | * @param L the longitude 198 | * @param Θ0 the sidereal time 199 | * @param α2 the right ascension 200 | * @return the approximate transite 201 | */ 202 | fun approximateTransit(L: Double, Θ0: Double, α2: Double): Double { 203 | /* Equation from page Astronomical Algorithms 102 */ 204 | val Lw = L * -1 205 | return normalizeWithBound((α2 + Lw - Θ0) / 360, 1.0) 206 | } 207 | 208 | /** 209 | * The time at which the sun is at its highest point in the sky (in universal time) 210 | * @param m0 approximate transit 211 | * @param L the longitude 212 | * @param Θ0 the sidereal time 213 | * @param α2 the right ascension 214 | * @param α1 the previous right ascension 215 | * @param α3 the next right ascension 216 | * @return the time (in universal time) when the sun is at its highest point in the sky 217 | */ 218 | fun correctedTransit( 219 | m0: Double, 220 | L: Double, 221 | Θ0: Double, 222 | α2: Double, 223 | α1: Double, 224 | α3: Double 225 | ): Double { 226 | /* Equation from page Astronomical Algorithms 102 */ 227 | val Lw = L * -1 228 | val θ = unwindAngle(Θ0 + 360.985647 * m0) 229 | val α = unwindAngle( 230 | interpolateAngles( /* value */ 231 | α2, /* previousValue */α1, /* nextValue */α3, /* factor */m0 232 | ) 233 | ) 234 | val H = closestAngle(θ - Lw - α) 235 | val Δm = H / -360 236 | return (m0 + Δm) * 24 237 | } 238 | 239 | /** 240 | * Get the corrected hour angle 241 | * @param m0 the approximate transit 242 | * @param h0 the angle 243 | * @param coordinates the coordinates 244 | * @param afterTransit whether it's after transit 245 | * @param Θ0 the sidereal time 246 | * @param α2 the right ascension 247 | * @param α1 the previous right ascension 248 | * @param α3 the next right ascension 249 | * @param δ2 the declination 250 | * @param δ1 the previous declination 251 | * @param δ3 the next declination 252 | * @return the corrected hour angle 253 | */ 254 | fun correctedHourAngle( 255 | m0: Double, h0: Double, coordinates: Coordinates, afterTransit: Boolean, 256 | Θ0: Double, α2: Double, α1: Double, α3: Double, δ2: Double, δ1: Double, δ3: Double 257 | ): Double { 258 | /* Equation from page Astronomical Algorithms 102 */ 259 | val Lw = coordinates.longitude * -1 260 | val term1 = sin(h0.toRadians()) - sin(coordinates.latitude.toRadians()) * sin(δ2.toRadians()) 261 | val term2 = cos(coordinates.latitude.toRadians()) * cos(δ2.toRadians()) 262 | val H0: Double = acos(term1 / term2).toDegrees() 263 | val m = if (afterTransit) m0 + H0 / 360 else m0 - H0 / 360 264 | val θ = unwindAngle(Θ0 + 360.985647 * m) 265 | val α = unwindAngle( 266 | interpolateAngles( /* value */ 267 | α2, /* previousValue */α1, /* nextValue */α3, /* factor */m 268 | ) 269 | ) 270 | val δ = interpolate( /* value */δ2, /* previousValue */δ1, /* nextValue */ 271 | δ3, /* factor */m 272 | ) 273 | val H = θ - Lw - α 274 | val h = altitudeOfCelestialBody( /* observerLatitude */coordinates.latitude, /* declination */ 275 | δ, /* localHourAngle */H 276 | ) 277 | val term3 = h - h0 278 | val term4 = 360 * cos(δ.toRadians()) * cos(coordinates.latitude.toRadians()) * sin(H.toRadians()) 279 | val Δm = term3 / term4 280 | return (m + Δm) * 24 281 | } 282 | 283 | /** 284 | * Interpolation of a value given equidistant 285 | * previous and next values and a factor 286 | * equal to the fraction of the interpolated 287 | * point's time over the time between values. 288 | * 289 | * @param y2 the value 290 | * @param y1 the previous value 291 | * @param y3 the next value 292 | * @param n the factor 293 | * @return the interpolated value 294 | */ 295 | fun interpolate(y2: Double, y1: Double, y3: Double, n: Double): Double { 296 | /* Equation from Astronomical Algorithms page 24 */ 297 | val a = y2 - y1 298 | val b = y3 - y2 299 | val c = b - a 300 | return y2 + n / 2 * (a + b + n * c) 301 | } 302 | 303 | /** 304 | * Interpolation of three angles, accounting for angle unwinding 305 | * @param y2 value 306 | * @param y1 previousValue 307 | * @param y3 nextValue 308 | * @param n factor 309 | * @return interpolated angle 310 | */ 311 | fun interpolateAngles(y2: Double, y1: Double, y3: Double, n: Double): Double { 312 | /* Equation from Astronomical Algorithms page 24 */ 313 | val a = unwindAngle(y2 - y1) 314 | val b = unwindAngle(y3 - y2) 315 | val c = b - a 316 | return y2 + n / 2 * (a + b + n * c) 317 | } 318 | } -------------------------------------------------------------------------------- /adhan/src/commonTest/kotlin/com/batoulapps/adhan2/internal/AstronomicalTest.kt: -------------------------------------------------------------------------------- 1 | package com.batoulapps.adhan2.internal 2 | 3 | import com.batoulapps.adhan2.Coordinates 4 | import com.batoulapps.adhan2.data.CalendarUtil.isLeapYear 5 | import com.batoulapps.adhan2.data.DateComponents 6 | import com.batoulapps.adhan2.data.TimeComponents 7 | import com.batoulapps.adhan2.internal.Astronomical.altitudeOfCelestialBody 8 | import com.batoulapps.adhan2.internal.Astronomical.apparentObliquityOfTheEcliptic 9 | import com.batoulapps.adhan2.internal.Astronomical.apparentSolarLongitude 10 | import com.batoulapps.adhan2.internal.Astronomical.approximateTransit 11 | import com.batoulapps.adhan2.internal.Astronomical.ascendingLunarNodeLongitude 12 | import com.batoulapps.adhan2.internal.Astronomical.correctedHourAngle 13 | import com.batoulapps.adhan2.internal.Astronomical.correctedTransit 14 | import com.batoulapps.adhan2.internal.Astronomical.interpolate 15 | import com.batoulapps.adhan2.internal.Astronomical.interpolateAngles 16 | import com.batoulapps.adhan2.internal.Astronomical.meanLunarLongitude 17 | import com.batoulapps.adhan2.internal.Astronomical.meanObliquityOfTheEcliptic 18 | import com.batoulapps.adhan2.internal.Astronomical.meanSiderealTime 19 | import com.batoulapps.adhan2.internal.Astronomical.meanSolarAnomaly 20 | import com.batoulapps.adhan2.internal.Astronomical.meanSolarLongitude 21 | import com.batoulapps.adhan2.internal.Astronomical.nutationInLongitude 22 | import com.batoulapps.adhan2.internal.Astronomical.nutationInObliquity 23 | import com.batoulapps.adhan2.internal.Astronomical.solarEquationOfTheCenter 24 | import com.batoulapps.adhan2.internal.CalendricalHelper.julianCentury 25 | import com.batoulapps.adhan2.internal.CalendricalHelper.julianDay 26 | import com.batoulapps.adhan2.internal.DoubleUtil.unwindAngle 27 | import kotlinx.datetime.DateTimeUnit 28 | import kotlin.math.abs 29 | import kotlin.math.roundToInt 30 | import kotlin.test.Test 31 | import kotlin.test.assertEquals 32 | import kotlin.test.assertFalse 33 | import kotlin.test.assertTrue 34 | 35 | class AstronomicalTest { 36 | 37 | @Test 38 | fun testSolarCoordinates() { 39 | // values from Astronomical Algorithms page 165 40 | var jd = julianDay( /* year */1992, /* month */10, /* day */13) 41 | var solar = SolarCoordinates( /* julianDay */jd) 42 | 43 | var T = julianCentury( /* julianDay */jd) 44 | var L0 = meanSolarLongitude( /* julianCentury */T) 45 | var ε0 = meanObliquityOfTheEcliptic( /* julianCentury */T) 46 | val εapp = apparentObliquityOfTheEcliptic( /* julianCentury */ 47 | T, /* meanObliquityOfTheEcliptic */ε0 48 | ) 49 | val M = meanSolarAnomaly( /* julianCentury */T) 50 | val C = solarEquationOfTheCenter( /* julianCentury */ 51 | T, /* meanAnomaly */M 52 | ) 53 | val λ = apparentSolarLongitude( /* julianCentury */ 54 | T, /* meanLongitude */L0 55 | ) 56 | val δ = solar.declination 57 | val α = unwindAngle(solar.rightAscension) 58 | 59 | // assertTrue { abs(params.fajrAngle - 18) <= 0.000001 } 60 | 61 | assertTrue { abs(T - -0.072183436) <= 0.00000000001 } 62 | assertTrue { abs(L0 - 201.80720) <= 0.00001 } 63 | assertTrue { abs(ε0 - 23.44023) <= 0.00001 } 64 | assertTrue { abs(εapp - 23.43999) <= 0.00001 } 65 | assertTrue { abs(M - 278.99397) <= 0.00001 } 66 | assertTrue { abs(C - -1.89732) <= 0.00001 } 67 | 68 | // lower accuracy than desired 69 | assertTrue { abs(λ - 199.90895) <= 0.00002 } 70 | assertTrue { abs(δ - -7.78507) <= 0.00001 } 71 | assertTrue { abs(α - 198.38083) <= 0.00001 } 72 | 73 | // values from Astronomical Algorithms page 88 74 | jd = julianDay( /* year */1987, /* month */4, /* day */10) 75 | solar = SolarCoordinates( /* julianDay */jd) 76 | T = julianCentury( /* julianDay */jd) 77 | val θ0 = meanSiderealTime( /* julianCentury */T) 78 | val θapp = solar.apparentSiderealTime 79 | val Ω = ascendingLunarNodeLongitude( /* julianCentury */T) 80 | ε0 = meanObliquityOfTheEcliptic( /* julianCentury */T) 81 | L0 = meanSolarLongitude( /* julianCentury */T) 82 | val Lp = meanLunarLongitude( /* julianCentury */T) 83 | val ΔΨ = nutationInLongitude( /* julianCentury */T, /* solarLongitude */ 84 | L0, /* lunarLongitude */Lp, /* ascendingNode */Ω 85 | ) 86 | val Δε = nutationInObliquity( /* julianCentury */T, /* solarLongitude */ 87 | L0, /* lunarLongitude */Lp, /* ascendingNode */Ω 88 | ) 89 | val ε = ε0 + Δε 90 | 91 | assertTrue { abs(θ0 - 197.693195) <= 0.000001 } 92 | assertTrue { abs(θapp - 197.6922295833) <= 0.0001 } 93 | 94 | // values from Astronomical Algorithms page 148 95 | assertTrue { abs(Ω - 11.2531) <= 0.0001 } 96 | assertTrue { abs(ΔΨ - -0.0010522) <= 0.0001 } 97 | assertTrue { abs(Δε - 0.0026230556) <= 0.00001 } 98 | assertTrue { abs(ε0 - 23.4409463889) <= 0.000001 } 99 | assertTrue { abs(ε - 23.4435694444) <= 0.00001 } 100 | } 101 | 102 | @Test 103 | fun testRightAscensionEdgeCase() { 104 | lateinit var previousTime: SolarTime 105 | val coordinates = Coordinates(35 + 47.0 / 60.0, -78 - 39.0 / 60.0) 106 | for (i in 0..364) { 107 | val time = SolarTime( 108 | TestUtils.makeDateWithOffset(2016, 1, 1, i, DateTimeUnit.DAY), coordinates 109 | ) 110 | 111 | if (i > 0) { 112 | // transit from one day to another should not differ more than one minute 113 | assertTrue(abs(time.transit - previousTime.transit) < (1.0 / 60.0)) 114 | 115 | // sunrise and sunset from one day to another should not differ more than two minutes 116 | assertTrue(abs(time.sunrise - previousTime.sunrise) < (2.0 / 60.0)) 117 | assertTrue(abs(time.sunset - previousTime.sunset) < (2.0 / 60.0)) 118 | } 119 | previousTime = time 120 | } 121 | } 122 | 123 | @Test 124 | fun testAltitudeOfCelestialBody() { 125 | val φ = 38 + 55 / 60.0 + 17.0 / 3600 126 | val δ = -6 - 43 / 60.0 - 11.61 / 3600 127 | val H = 64.352133 128 | val h = altitudeOfCelestialBody( /* observerLatitude */ 129 | φ, /* declination */δ, /* localHourAngle */H 130 | ) 131 | assertTrue { abs(h - 15.1249) <= 0.0001 } 132 | } 133 | 134 | @Test 135 | fun testTransitAndHourAngle() { 136 | // values from Astronomical Algorithms page 103 137 | val longitude = -71.0833 138 | val Θ = 177.74208 139 | val α1 = 40.68021 140 | val α2 = 41.73129 141 | val α3 = 42.78204 142 | val m0 = approximateTransit( 143 | longitude, /* siderealTime */ 144 | Θ, /* rightAscension */α2 145 | ) 146 | assertTrue { abs(m0 - 0.81965) <= 0.00001 } 147 | val transit = correctedTransit( /* approximateTransit */ 148 | m0, longitude, /* siderealTime */Θ, /* rightAscension */ 149 | α2, /* previousRightAscension */α1, /* nextRightAscension */ 150 | α3 151 | ) / 24 152 | assertTrue { abs(transit - 0.81980) <= 0.00001 } 153 | val δ1 = 18.04761 154 | val δ2 = 18.44092 155 | val δ3 = 18.82742 156 | val rise = correctedHourAngle( /* approximateTransit */m0, /* angle */ 157 | -0.5667, Coordinates( /* latitude */42.3333, longitude), /* afterTransit */ 158 | false, /* siderealTime */Θ, /* rightAscension */ 159 | α2, /* previousRightAscension */α1, /* nextRightAscension */ 160 | α3, /* declination */δ2, /* previousDeclination */ 161 | δ1, /* nextDeclination */δ3 162 | ) / 24 163 | assertTrue { abs(rise - 0.51766) <= 0.00001 } 164 | } 165 | 166 | @Test 167 | fun testSolarTime() { 168 | /* 169 | * Comparison values generated from 170 | * http://aa.usno.navy.mil/rstt/onedaytable?form=1&ID=AA&year=2015&month=7&day=12&state=NC&place=raleigh 171 | */ 172 | val coordinates = Coordinates(35 + 47.0 / 60.0, -78 - 39.0 / 60.0) 173 | val solar = SolarTime(DateComponents(2015, 7, 12), coordinates) 174 | val transit = solar.transit 175 | val sunrise = solar.sunrise 176 | val sunset = solar.sunset 177 | val twilightStart = solar.timeForSolarAngle(-6.0, /* afterTransit */false) 178 | val twilightEnd = solar.timeForSolarAngle(-6.0, /* afterTransit */true) 179 | val invalid = solar.timeForSolarAngle(-36.0, /* afterTransit */true) 180 | 181 | assertEquals("9:38", timeString(twilightStart)) 182 | assertEquals("10:08", timeString(sunrise)) 183 | assertEquals("17:20", timeString(transit)) 184 | assertEquals("24:32", timeString(sunset)) 185 | assertEquals("25:02", timeString(twilightEnd)) 186 | assertEquals("", timeString(invalid)) 187 | } 188 | 189 | private fun timeString(whence: Double): String { 190 | val components = TimeComponents.fromDouble(whence) ?: return "" 191 | val minutes = (components.minutes + (components.seconds / 60.0).roundToInt()) 192 | val paddedMinutes = if (minutes < 10) "0$minutes" else "$minutes" 193 | return "${components.hours}:$paddedMinutes" 194 | } 195 | 196 | @Test 197 | fun testCalendricalDate() { 198 | // generated from http://aa.usno.navy.mil/data/docs/RS_OneYear.php for KUKUIHAELE, HAWAII 199 | val coordinates = Coordinates( /* latitude */ 200 | 20 + 7.0 / 60.0, /* longitude */-155.0 - 34.0 / 60.0 201 | ) 202 | val day1solar = SolarTime(DateComponents(2015, 4, /* day */2), coordinates) 203 | val day2solar = SolarTime(DateComponents(2015, 4, 3), coordinates) 204 | val day1 = day1solar.sunrise 205 | val day2 = day2solar.sunrise 206 | assertEquals("16:15", timeString(day1)) 207 | assertEquals("16:14", timeString(day2)) 208 | } 209 | 210 | @Test 211 | fun testInterpolation() { 212 | // values from Astronomical Algorithms page 25 213 | val interpolatedValue = interpolate( /* value */0.877366, /* previousValue */ 214 | 0.884226, /* nextValue */0.870531, /* factor */4.35 / 24 215 | ) 216 | assertTrue { abs(interpolatedValue - 0.876125) <= 0.000001 } 217 | val i1 = interpolate(1.0, -1.0, 3.0, /* factor */0.6) 218 | assertTrue { abs(i1 - 2.2) <= 0.000001 } 219 | } 220 | 221 | @Test 222 | fun testAngleInterpolation() { 223 | val i1 = interpolateAngles(1.0, -1.0, 3.0, /* factor */0.6) 224 | assertTrue { abs(i1 - 2.2) <= 0.000001 } 225 | val i2 = interpolateAngles(1.0, 359.0, 3.0, /* factor */0.6) 226 | assertTrue { abs(i2 - 2.2) <= 0.000001 } 227 | } 228 | 229 | @Test 230 | fun testJulianDay() { 231 | /* 232 | * Comparison values generated from http://aa.usno.navy.mil/data/docs/JulianDate.php 233 | */ 234 | assertTrue { abs(julianDay( /* year */2010, /* month */1, /* day */2) - 2455198.500000) <= 0.00001 } 235 | assertTrue { abs(julianDay( /* year */2011, /* month */2, /* day */4) - 2455596.500000) <= 0.00001 } 236 | assertTrue { abs(julianDay( /* year */2012, /* month */3, /* day */6) - 2455992.500000) <= 0.00001 } 237 | assertTrue { abs(julianDay( /* year */2013, /* month */4, /* day */8) - 2456390.500000) <= 0.00001 } 238 | assertTrue { abs(julianDay( /* year */2014, /* month */5, /* day */10) - 2456787.500000) <= 0.00001 } 239 | assertTrue { abs(julianDay( /* year */2015, /* month */6, /* day */12) - 2457185.500000) <= 0.00001 } 240 | assertTrue { abs(julianDay( /* year */2016, /* month */7, /* day */14) - 2457583.500000) <= 0.00001 } 241 | assertTrue { abs(julianDay( /* year */2017, /* month */8, /* day */16) - 2457981.500000) <= 0.00001 } 242 | assertTrue { abs(julianDay( /* year */2018, /* month */9, /* day */18) - 2458379.500000) <= 0.00001 } 243 | assertTrue { abs(julianDay( /* year */2019, /* month */10, /* day */20) - 2458776.500000) <= 0.00001 } 244 | assertTrue { abs(julianDay( /* year */2020, /* month */11, /* day */22) - 2459175.500000) <= 0.00001 } 245 | assertTrue { abs(julianDay( /* year */2021, /* month */12, /* day */24) - 2459572.500000) <= 0.00001 } 246 | 247 | val jdVal = 2457215.67708333 248 | assertTrue { 249 | abs(julianDay( /* year */2015, /* month */7, /* day */12, /* hours */4.25) - jdVal) <= 0.000001 250 | } 251 | 252 | val components = TestUtils.makeDate(year = 2015, month = 7, day = 12, hour = 4, minute = 15) 253 | assertTrue { abs(julianDay(components) - jdVal) <= 0.000001 } 254 | assertTrue{ abs(julianDay( year = 2015, month = 7, day = 12, hours = 8.0) - 2457215.833333) <= 0.000001 } 255 | assertTrue{ abs(julianDay( year = 1992, month = 10, day = 13, hours = 0.0) - 2448908.5) <= 0.000001 } 256 | } 257 | 258 | 259 | @Test 260 | fun testJulianHours() { 261 | val j1 = julianDay( /* year */2010, /* month */1, /* day */3) 262 | val j2 = julianDay( /* year */2010, /* month */ 263 | 1, /* day */1, 48.0 264 | ) 265 | assertTrue { abs(j1 - j2) <= 0.0000001 } 266 | } 267 | 268 | @Test 269 | fun testLeapYear() { 270 | assertFalse(isLeapYear(2015)) 271 | assertTrue(isLeapYear(2016)) 272 | assertTrue(isLeapYear(1600)) 273 | assertTrue(isLeapYear(2000)) 274 | assertTrue(isLeapYear(2400)) 275 | assertFalse(isLeapYear(1700)) 276 | assertFalse(isLeapYear(1800)) 277 | assertFalse(isLeapYear(1900)) 278 | assertFalse(isLeapYear(2100)) 279 | assertFalse(isLeapYear(2200)) 280 | assertFalse(isLeapYear(2300)) 281 | assertFalse(isLeapYear(2500)) 282 | assertFalse(isLeapYear(2600)) 283 | } 284 | } -------------------------------------------------------------------------------- /adhan/src/commonMain/kotlin/com/batoulapps/adhan2/PrayerTimes.kt: -------------------------------------------------------------------------------- 1 | package com.batoulapps.adhan2 2 | 3 | import com.batoulapps.adhan2.Prayer.ASR 4 | import com.batoulapps.adhan2.Prayer.DHUHR 5 | import com.batoulapps.adhan2.Prayer.FAJR 6 | import com.batoulapps.adhan2.Prayer.ISHA 7 | import com.batoulapps.adhan2.Prayer.MAGHRIB 8 | import com.batoulapps.adhan2.Prayer.NONE 9 | import com.batoulapps.adhan2.Prayer.SUNRISE 10 | import com.batoulapps.adhan2.data.CalendarUtil.add 11 | import com.batoulapps.adhan2.data.CalendarUtil.isLeapYear 12 | import com.batoulapps.adhan2.data.CalendarUtil.resolveTime 13 | import com.batoulapps.adhan2.data.CalendarUtil.roundedMinute 14 | import com.batoulapps.adhan2.data.CalendarUtil.toUtcInstant 15 | import com.batoulapps.adhan2.data.DateComponents 16 | import com.batoulapps.adhan2.data.TimeComponents 17 | import com.batoulapps.adhan2.internal.SolarTime 18 | import com.batoulapps.adhan2.model.Shafaq 19 | import kotlin.math.abs 20 | import kotlin.math.roundToInt 21 | import kotlinx.datetime.DateTimeUnit 22 | import kotlinx.datetime.LocalDateTime 23 | import kotlinx.datetime.TimeZone 24 | import kotlinx.datetime.toInstant 25 | import kotlin.time.Instant 26 | 27 | /** 28 | * Calculate PrayerTimes 29 | * @param coordinates the coordinates of the location 30 | * @param dateComponents the date components for that location 31 | * @param calculationParameters the parameters for the calculation 32 | */ 33 | data class PrayerTimes( 34 | val coordinates: Coordinates, 35 | val dateComponents: DateComponents, 36 | val calculationParameters: CalculationParameters 37 | ) { 38 | val fajr: Instant 39 | val sunrise: Instant 40 | val dhuhr: Instant 41 | val asr: Instant 42 | val maghrib: Instant 43 | val isha: Instant 44 | 45 | init { 46 | var tempFajr: LocalDateTime? = null 47 | val tempSunrise: LocalDateTime? 48 | val tempDhuhr: LocalDateTime? 49 | var tempAsr: LocalDateTime? = null 50 | val tempMaghrib: LocalDateTime? 51 | var tempIsha: LocalDateTime? = null 52 | val prayerDate: LocalDateTime = resolveTime(dateComponents) 53 | 54 | val dayOfYear: Int = prayerDate.dayOfYear 55 | 56 | val tomorrowDate: LocalDateTime = add(prayerDate, 1, DateTimeUnit.DAY) 57 | val tomorrow: DateComponents = DateComponents.fromLocalDateTime(tomorrowDate) 58 | 59 | val solarTime = SolarTime(dateComponents, coordinates) 60 | var timeComponents = TimeComponents.fromDouble(solarTime.transit) 61 | val transit = timeComponents?.dateComponents(dateComponents) 62 | 63 | timeComponents = TimeComponents.fromDouble(solarTime.sunrise) 64 | val sunriseComponents = timeComponents?.dateComponents(dateComponents) 65 | 66 | timeComponents = TimeComponents.fromDouble(solarTime.sunset) 67 | val sunsetComponents = timeComponents?.dateComponents(dateComponents) 68 | 69 | val tomorrowSolarTime = SolarTime(tomorrow, coordinates) 70 | val tomorrowSunriseComponents = TimeComponents.fromDouble(tomorrowSolarTime.sunrise) 71 | 72 | if (transit == null || sunriseComponents == null || sunsetComponents == null || tomorrowSunriseComponents == null) { 73 | tempSunrise = null 74 | tempDhuhr = null 75 | tempAsr = null 76 | tempMaghrib = null 77 | } else { 78 | tempDhuhr = transit 79 | tempSunrise = sunriseComponents 80 | tempMaghrib = sunsetComponents 81 | timeComponents = TimeComponents.fromDouble( 82 | solarTime.afternoon(calculationParameters.madhab.shadowLength) 83 | ) 84 | 85 | if (timeComponents != null) { 86 | tempAsr = timeComponents.dateComponents(dateComponents) 87 | } 88 | 89 | // get night length 90 | val tomorrowSunrise = tomorrowSunriseComponents.dateComponents(tomorrow) 91 | val night = tomorrowSunrise.toInstant(TimeZone.UTC).toEpochMilliseconds() - 92 | sunsetComponents.toInstant(TimeZone.UTC).toEpochMilliseconds() 93 | 94 | timeComponents = TimeComponents.fromDouble( 95 | solarTime.timeForSolarAngle(-calculationParameters.fajrAngle, false)) 96 | if (timeComponents != null) { 97 | tempFajr = timeComponents.dateComponents(dateComponents) 98 | } 99 | 100 | // special case for moonsighting committee above latitude 55 101 | if (calculationParameters.method === CalculationMethod.MOON_SIGHTING_COMMITTEE && 102 | coordinates.latitude >= 55 103 | ) { 104 | tempFajr = add( 105 | sunriseComponents, -1 * (night / 7000).toInt(), DateTimeUnit.SECOND 106 | ) 107 | } 108 | 109 | val nightPortions = calculationParameters.nightPortions(coordinates) 110 | 111 | val safeFajr: LocalDateTime = 112 | if (calculationParameters.method === CalculationMethod.MOON_SIGHTING_COMMITTEE) { 113 | seasonAdjustedMorningTwilight( 114 | coordinates.latitude, 115 | dayOfYear, 116 | dateComponents.year, 117 | sunriseComponents 118 | ) 119 | } else { 120 | val portion = nightPortions.fajr 121 | val nightFraction = (portion * night / 1000).toLong() 122 | add( 123 | sunriseComponents, -1 * nightFraction.toInt(), DateTimeUnit.SECOND 124 | ) 125 | } 126 | 127 | if (tempFajr == null || tempFajr.before(safeFajr)) { 128 | tempFajr = safeFajr 129 | } 130 | 131 | // Isha calculation with check against safe value 132 | if (calculationParameters.ishaInterval > 0) { 133 | tempIsha = add(tempMaghrib, calculationParameters.ishaInterval * 60, DateTimeUnit.SECOND) 134 | } else { 135 | timeComponents = TimeComponents.fromDouble( 136 | solarTime.timeForSolarAngle(-calculationParameters.ishaAngle, true) 137 | ) 138 | if (timeComponents != null) { 139 | tempIsha = timeComponents.dateComponents(dateComponents) 140 | } 141 | 142 | // special case for moonsighting committee above latitude 55 143 | if (calculationParameters.method === CalculationMethod.MOON_SIGHTING_COMMITTEE && 144 | coordinates.latitude >= 55 145 | ) { 146 | val nightFraction = night / 7000 147 | tempIsha = add(sunsetComponents, nightFraction.toInt(), DateTimeUnit.SECOND) 148 | } 149 | 150 | val safeIsha: LocalDateTime = if (calculationParameters.method === CalculationMethod.MOON_SIGHTING_COMMITTEE) { 151 | seasonAdjustedEveningTwilight( 152 | coordinates.latitude, dayOfYear, dateComponents.year, sunsetComponents, calculationParameters.shafaq 153 | ) 154 | } else { 155 | val portion = nightPortions.isha 156 | val nightFraction = (portion * night / 1000).toLong() 157 | add(sunsetComponents, nightFraction.toInt(), DateTimeUnit.SECOND) 158 | } 159 | 160 | if (tempIsha == null || tempIsha.after(safeIsha)) { 161 | tempIsha = safeIsha 162 | } 163 | } 164 | } 165 | 166 | if (tempFajr == null || tempSunrise == null || tempDhuhr == null || tempAsr == null || tempMaghrib == null || tempIsha == null) { 167 | // if we don't have all prayer times then initialization failed 168 | throw IllegalStateException() 169 | } else { 170 | // Assign final times to public struct members with all offsets 171 | fajr = roundedMinute( 172 | add( 173 | add(tempFajr, calculationParameters.prayerAdjustments.fajr, DateTimeUnit.MINUTE), 174 | calculationParameters.methodAdjustments.fajr, 175 | DateTimeUnit.MINUTE 176 | ), 177 | rounding = calculationParameters.rounding 178 | ).toUtcInstant() 179 | sunrise = roundedMinute( 180 | add( 181 | add(tempSunrise, calculationParameters.prayerAdjustments.sunrise, DateTimeUnit.MINUTE), 182 | calculationParameters.methodAdjustments.sunrise, 183 | DateTimeUnit.MINUTE 184 | ), 185 | rounding = calculationParameters.rounding 186 | ).toUtcInstant() 187 | dhuhr = roundedMinute( 188 | add( 189 | add(tempDhuhr, calculationParameters.prayerAdjustments.dhuhr, DateTimeUnit.MINUTE), 190 | calculationParameters.methodAdjustments.dhuhr, 191 | DateTimeUnit.MINUTE 192 | ), 193 | rounding = calculationParameters.rounding 194 | ).toUtcInstant() 195 | asr = roundedMinute( 196 | add( 197 | add(tempAsr, calculationParameters.prayerAdjustments.asr, DateTimeUnit.MINUTE), 198 | calculationParameters.methodAdjustments.asr, 199 | DateTimeUnit.MINUTE 200 | ), 201 | rounding = calculationParameters.rounding 202 | ).toUtcInstant() 203 | maghrib = roundedMinute( 204 | add( 205 | add(tempMaghrib, calculationParameters.prayerAdjustments.maghrib, DateTimeUnit.MINUTE), 206 | calculationParameters.methodAdjustments.maghrib, 207 | DateTimeUnit.MINUTE 208 | ), 209 | rounding = calculationParameters.rounding 210 | ).toUtcInstant() 211 | isha = roundedMinute( 212 | add( 213 | add(tempIsha, calculationParameters.prayerAdjustments.isha, DateTimeUnit.MINUTE), 214 | calculationParameters.methodAdjustments.isha, 215 | DateTimeUnit.MINUTE 216 | ), 217 | rounding = calculationParameters.rounding 218 | ).toUtcInstant() 219 | } 220 | } 221 | 222 | fun currentPrayer(instant: Instant): Prayer { 223 | return when { 224 | instant >= isha -> { ISHA } 225 | instant >= maghrib -> { MAGHRIB } 226 | instant >= asr -> { ASR } 227 | instant >= dhuhr -> { DHUHR } 228 | instant >= sunrise -> { SUNRISE } 229 | instant >= fajr -> { FAJR } 230 | else -> { NONE } 231 | } 232 | } 233 | 234 | fun nextPrayer(instant: Instant): Prayer { 235 | return when { 236 | instant >= isha -> { NONE } 237 | instant >= maghrib -> { ISHA } 238 | instant >= asr -> { MAGHRIB } 239 | instant >= dhuhr -> { ASR } 240 | instant >= sunrise -> { DHUHR } 241 | instant >= fajr -> { SUNRISE } 242 | else -> { FAJR } 243 | } 244 | } 245 | 246 | fun timeForPrayer(prayer: Prayer): Instant? { 247 | return when (prayer) { 248 | FAJR -> fajr 249 | SUNRISE -> sunrise 250 | DHUHR -> dhuhr 251 | ASR -> asr 252 | MAGHRIB -> maghrib 253 | ISHA -> isha 254 | NONE -> null 255 | } 256 | } 257 | 258 | private fun LocalDateTime.before(other: LocalDateTime): Boolean { 259 | return toInstant(TimeZone.UTC).toEpochMilliseconds() < 260 | other.toInstant(TimeZone.UTC).toEpochMilliseconds() 261 | } 262 | 263 | private fun LocalDateTime.after(other: LocalDateTime): Boolean { 264 | return toInstant(TimeZone.UTC).toEpochMilliseconds() > 265 | other.toInstant(TimeZone.UTC).toEpochMilliseconds() 266 | } 267 | 268 | companion object { 269 | private fun seasonAdjustedMorningTwilight( 270 | latitude: Double, day: Int, year: Int, sunrise: LocalDateTime 271 | ): LocalDateTime { 272 | val a: Double = 75 + 28.65 / 55.0 * abs(latitude) 273 | val b: Double = 75 + 19.44 / 55.0 * abs(latitude) 274 | val c: Double = 75 + 32.74 / 55.0 * abs(latitude) 275 | val d: Double = 75 + 48.10 / 55.0 * abs(latitude) 276 | val dyy = daysSinceSolstice(day, year, latitude) 277 | val adjustment = when { 278 | dyy < 91 -> { a + (b - a) / 91.0 * dyy } 279 | dyy < 137 -> { b + (c - b) / 46.0 * (dyy - 91) } 280 | dyy < 183 -> { c + (d - c) / 46.0 * (dyy - 137) } 281 | dyy < 229 -> { d + (c - d) / 46.0 * (dyy - 183) } 282 | dyy < 275 -> { c + (b - c) / 46.0 * (dyy - 229) } 283 | else -> { b + (a - b) / 91.0 * (dyy - 275) } 284 | } 285 | return add(sunrise, -(adjustment * 60.0).roundToInt(), DateTimeUnit.SECOND) 286 | } 287 | 288 | private fun seasonAdjustedEveningTwilight( 289 | latitude: Double, day: Int, year: Int, sunset: LocalDateTime, shafaq: Shafaq 290 | ): LocalDateTime { 291 | 292 | val a: Double 293 | val b: Double 294 | val c: Double 295 | val d: Double 296 | when (shafaq) { 297 | Shafaq.GENERAL -> { 298 | a = 75 + 25.60 / 55.0 * abs(latitude) 299 | b = 75 + 2.050 / 55.0 * abs(latitude) 300 | c = 75 - 9.210 / 55.0 * abs(latitude) 301 | d = 75 + 6.140 / 55.0 * abs(latitude) 302 | } 303 | Shafaq.AHMER -> { 304 | a = 62 + ((17.40 / 55.0) * abs(latitude)) 305 | b = 62 - ((7.160 / 55.0) * abs(latitude)) 306 | c = 62 + ((5.120 / 55.0) * abs(latitude)) 307 | d = 62 + ((19.44 / 55.0) * abs(latitude)) 308 | } 309 | Shafaq.ABYAD -> { 310 | a = 75 + ((25.60 / 55.0) * abs(latitude)) 311 | b = 75 + ((7.160 / 55.0) * abs(latitude)) 312 | c = 75 + ((36.84 / 55.0) * abs(latitude)) 313 | d = 75 + ((81.84 / 55.0) * abs(latitude)) 314 | } 315 | } 316 | 317 | val dyy = daysSinceSolstice(day, year, latitude) 318 | val adjustment = when { 319 | dyy < 91 -> { a + (b - a) / 91.0 * dyy } 320 | dyy < 137 -> { b + (c - b) / 46.0 * (dyy - 91) } 321 | dyy < 183 -> { c + (d - c) / 46.0 * (dyy - 137) } 322 | dyy < 229 -> { d + (c - d) / 46.0 * (dyy - 183) } 323 | dyy < 275 -> { c + (b - c) / 46.0 * (dyy - 229) } 324 | else -> { b + (a - b) / 91.0 * (dyy - 275) } 325 | } 326 | return add(sunset, (adjustment * 60.0).roundToInt(), DateTimeUnit.SECOND) 327 | } 328 | 329 | fun daysSinceSolstice(dayOfYear: Int, year: Int, latitude: Double): Int { 330 | var daysSinceSolistice: Int 331 | val northernOffset = 10 332 | val isLeapYear = isLeapYear(year) 333 | val southernOffset = if (isLeapYear) 173 else 172 334 | val daysInYear = if (isLeapYear) 366 else 365 335 | if (latitude >= 0) { 336 | daysSinceSolistice = dayOfYear + northernOffset 337 | if (daysSinceSolistice >= daysInYear) { 338 | daysSinceSolistice -= daysInYear 339 | } 340 | } else { 341 | daysSinceSolistice = dayOfYear - southernOffset 342 | if (daysSinceSolistice < 0) { 343 | daysSinceSolistice += daysInYear 344 | } 345 | } 346 | return daysSinceSolistice 347 | } 348 | } 349 | } 350 | -------------------------------------------------------------------------------- /adhan/src/commonTest/kotlin/com/batoulapps/adhan2/PrayerTimesTest.kt: -------------------------------------------------------------------------------- 1 | package com.batoulapps.adhan2 2 | 3 | import com.batoulapps.adhan2.CalculationMethod.KARACHI 4 | import com.batoulapps.adhan2.CalculationMethod.MOON_SIGHTING_COMMITTEE 5 | import com.batoulapps.adhan2.CalculationMethod.MUSLIM_WORLD_LEAGUE 6 | import com.batoulapps.adhan2.CalculationMethod.NORTH_AMERICA 7 | import com.batoulapps.adhan2.HighLatitudeRule.MIDDLE_OF_THE_NIGHT 8 | import com.batoulapps.adhan2.HighLatitudeRule.SEVENTH_OF_THE_NIGHT 9 | import com.batoulapps.adhan2.HighLatitudeRule.TWILIGHT_ANGLE 10 | import com.batoulapps.adhan2.Madhab.HANAFI 11 | import com.batoulapps.adhan2.Prayer.ASR 12 | import com.batoulapps.adhan2.Prayer.DHUHR 13 | import com.batoulapps.adhan2.Prayer.FAJR 14 | import com.batoulapps.adhan2.Prayer.ISHA 15 | import com.batoulapps.adhan2.Prayer.MAGHRIB 16 | import com.batoulapps.adhan2.Prayer.NONE 17 | import com.batoulapps.adhan2.Prayer.SUNRISE 18 | import com.batoulapps.adhan2.data.DateComponents 19 | import com.batoulapps.adhan2.internal.TestUtils.addSeconds 20 | import com.batoulapps.adhan2.internal.TestUtils.makeDate 21 | import com.batoulapps.adhan2.internal.TestUtils.pad 22 | import com.batoulapps.adhan2.model.Shafaq 23 | import kotlin.test.Test 24 | import kotlin.test.assertEquals 25 | import kotlin.test.assertFailsWith 26 | import kotlin.test.assertNotNull 27 | import kotlin.test.assertNull 28 | import kotlinx.datetime.TimeZone 29 | import kotlinx.datetime.toLocalDateTime 30 | import kotlin.time.Instant 31 | 32 | class PrayerTimesTest { 33 | 34 | @Test 35 | fun testDaysSinceSolstice() { 36 | daysSinceSolsticeTest(11, year = 2016, month = 1, day = 1, latitude = 1.0) 37 | daysSinceSolsticeTest(10, year = 2015, month = 12, day = 31, latitude = 1.0) 38 | daysSinceSolsticeTest(10, year = 2016, month = 12, day = 31, latitude = 1.0) 39 | daysSinceSolsticeTest(0, year = 2016, month = 12, day = 21, latitude = 1.0) 40 | daysSinceSolsticeTest(1, year = 2016, month = 12, day = 22, latitude = 1.0) 41 | daysSinceSolsticeTest(71, year = 2016, month = 3, day = 1, latitude = 1.0) 42 | daysSinceSolsticeTest(70, year = 2015, month = 3, day = 1, latitude = 1.0) 43 | daysSinceSolsticeTest(365, year = 2016, month = 12, day = 20, latitude = 1.0) 44 | daysSinceSolsticeTest(364, year = 2015, month = 12, day = 20, latitude = 1.0) 45 | daysSinceSolsticeTest(0, year = 2015, month = 6, day = 21, latitude = -1.0) 46 | daysSinceSolsticeTest(0, year = 2016, month = 6, day = 21, latitude = -1.0) 47 | daysSinceSolsticeTest(364, year = 2015, month = 6, day = 20, latitude = -1.0) 48 | daysSinceSolsticeTest(365, year = 2016, month = 6, day = 20, latitude = -1.0) 49 | } 50 | 51 | private fun daysSinceSolsticeTest(value: Int, year: Int, month: Int, day: Int, latitude: Double) { 52 | // For Northern Hemisphere start from December 21 53 | // (DYY=0 for December 21, and counting forward, DYY=11 for January 1 and so on). 54 | // For Southern Hemisphere start from June 21 55 | // (DYY=0 for June 21, and counting forward) 56 | val localDateTime = makeDate(year, month, day) 57 | val dayOfYear: Int = localDateTime.dayOfYear 58 | assertEquals(value, PrayerTimes.daysSinceSolstice(dayOfYear, localDateTime.year, latitude)) 59 | } 60 | 61 | private fun stringifyAtTimezone(time: Instant, zoneId: String): String { 62 | val timeZone = TimeZone.of(zoneId) 63 | val localDateTime = time.toLocalDateTime(timeZone) 64 | 65 | // hour is 0-23 66 | val initialHour = localDateTime.hour 67 | val mappedHour = when { 68 | initialHour == 0 -> 12 69 | initialHour > 12 -> initialHour - 12 70 | else -> initialHour 71 | } 72 | val hour = pad(mappedHour) 73 | val minutes = pad(localDateTime.minute) 74 | val amPM = if (initialHour >= 12) "PM" else "AM" 75 | return "$hour:$minutes $amPM" 76 | } 77 | 78 | @Test 79 | fun testPrayerTimes() { 80 | val date = DateComponents(2015, 7, 12) 81 | val params = NORTH_AMERICA.parameters.copy(madhab = HANAFI) 82 | 83 | val coordinates = Coordinates(35.7750, -78.6336) 84 | val prayerTimes = PrayerTimes(coordinates, date, params) 85 | 86 | val zoneId = "America/New_York" 87 | assertEquals("04:42 AM", stringifyAtTimezone(prayerTimes.fajr, zoneId)) 88 | assertEquals("06:08 AM", stringifyAtTimezone(prayerTimes.sunrise, zoneId)) 89 | assertEquals("01:21 PM", stringifyAtTimezone(prayerTimes.dhuhr, zoneId)) 90 | assertEquals("06:22 PM", stringifyAtTimezone(prayerTimes.asr, zoneId)) 91 | assertEquals("08:32 PM", stringifyAtTimezone(prayerTimes.maghrib, zoneId)) 92 | assertEquals("09:57 PM", stringifyAtTimezone(prayerTimes.isha, zoneId)) 93 | } 94 | 95 | @Test 96 | fun testOffsets() { 97 | val date = DateComponents(2015, 12, 1) 98 | val coordinates = Coordinates(35.7750, -78.6336) 99 | 100 | val zoneId = "America/New_York" 101 | val parameters = MUSLIM_WORLD_LEAGUE.parameters 102 | 103 | var prayerTimes = PrayerTimes(coordinates, date, parameters) 104 | assertEquals("05:35 AM", stringifyAtTimezone(prayerTimes.fajr, zoneId)) 105 | assertEquals("07:06 AM", stringifyAtTimezone(prayerTimes.sunrise, zoneId)) 106 | assertEquals("12:05 PM", stringifyAtTimezone(prayerTimes.dhuhr, zoneId)) 107 | assertEquals("02:42 PM", stringifyAtTimezone(prayerTimes.asr, zoneId)) 108 | assertEquals("05:01 PM", stringifyAtTimezone(prayerTimes.maghrib, zoneId)) 109 | assertEquals("06:26 PM", stringifyAtTimezone(prayerTimes.isha, zoneId)) 110 | 111 | val params = parameters.copy( 112 | prayerAdjustments = parameters.prayerAdjustments.copy( 113 | fajr = 10, 114 | sunrise = 10, 115 | dhuhr = 10, 116 | asr = 10, 117 | maghrib = 10, 118 | isha = 10 119 | ) 120 | ) 121 | prayerTimes = PrayerTimes(coordinates, date, params) 122 | assertEquals("05:45 AM", stringifyAtTimezone(prayerTimes.fajr, zoneId)) 123 | assertEquals("07:16 AM", stringifyAtTimezone(prayerTimes.sunrise, zoneId)) 124 | assertEquals("12:15 PM", stringifyAtTimezone(prayerTimes.dhuhr, zoneId)) 125 | assertEquals("02:52 PM", stringifyAtTimezone(prayerTimes.asr, zoneId)) 126 | assertEquals("05:11 PM", stringifyAtTimezone(prayerTimes.maghrib, zoneId)) 127 | assertEquals("06:36 PM", stringifyAtTimezone(prayerTimes.isha, zoneId)) 128 | 129 | prayerTimes = PrayerTimes(coordinates, date, 130 | params.copy(prayerAdjustments = PrayerAdjustments())) 131 | assertEquals("05:35 AM", stringifyAtTimezone(prayerTimes.fajr, zoneId)) 132 | assertEquals("07:06 AM", stringifyAtTimezone(prayerTimes.sunrise, zoneId)) 133 | assertEquals("12:05 PM", stringifyAtTimezone(prayerTimes.dhuhr, zoneId)) 134 | assertEquals("02:42 PM", stringifyAtTimezone(prayerTimes.asr, zoneId)) 135 | assertEquals("05:01 PM", stringifyAtTimezone(prayerTimes.maghrib, zoneId)) 136 | assertEquals("06:26 PM", stringifyAtTimezone(prayerTimes.isha, zoneId)) 137 | } 138 | 139 | @Test 140 | fun testMoonsightingMethod() { 141 | val date = DateComponents(2016, 1, 31) 142 | val coordinates = Coordinates(35.7750, -78.6336) 143 | val prayerTimes = PrayerTimes( 144 | coordinates, date, MOON_SIGHTING_COMMITTEE.parameters 145 | ) 146 | 147 | val zoneId = "America/New_York" 148 | assertEquals("05:48 AM", stringifyAtTimezone(prayerTimes.fajr, zoneId)) 149 | assertEquals("07:16 AM", stringifyAtTimezone(prayerTimes.sunrise, zoneId)) 150 | assertEquals("12:33 PM", stringifyAtTimezone(prayerTimes.dhuhr, zoneId)) 151 | assertEquals("03:20 PM", stringifyAtTimezone(prayerTimes.asr, zoneId)) 152 | assertEquals("05:43 PM", stringifyAtTimezone(prayerTimes.maghrib, zoneId)) 153 | assertEquals("07:05 PM", stringifyAtTimezone(prayerTimes.isha, zoneId)) 154 | } 155 | 156 | @Test 157 | fun testMoonsightingMethodHighLat() { 158 | // Values from http://www.moonsighting.com/pray.php 159 | val date = DateComponents(2016, 1, 1) 160 | val parameters = MOON_SIGHTING_COMMITTEE.parameters.copy(madhab = HANAFI) 161 | val coordinates = Coordinates(59.9094, 10.7349) 162 | 163 | val zoneId = "Europe/Oslo" 164 | val prayerTimes = PrayerTimes(coordinates, date, parameters) 165 | assertEquals("07:34 AM", stringifyAtTimezone(prayerTimes.fajr, zoneId)) 166 | assertEquals("09:19 AM", stringifyAtTimezone(prayerTimes.sunrise, zoneId)) 167 | assertEquals("12:25 PM", stringifyAtTimezone(prayerTimes.dhuhr, zoneId)) 168 | assertEquals("01:36 PM", stringifyAtTimezone(prayerTimes.asr, zoneId)) 169 | assertEquals("03:25 PM", stringifyAtTimezone(prayerTimes.maghrib, zoneId)) 170 | assertEquals("05:02 PM", stringifyAtTimezone(prayerTimes.isha, zoneId)) 171 | } 172 | 173 | @Test 174 | fun testDiyanet() { 175 | // values from https://namazvakitleri.diyanet.gov.tr/en-US/9541/prayer-time-for-istanbul 176 | val date = DateComponents(2020, 4, 16) 177 | val parameters = CalculationMethod.TURKEY.parameters 178 | val coordinates = Coordinates(41.005616, 28.976380) 179 | 180 | val zoneId = "Europe/Istanbul" 181 | val prayerTimes = PrayerTimes(coordinates, date, parameters) 182 | assertEquals("04:44 AM", stringifyAtTimezone(prayerTimes.fajr, zoneId)) 183 | assertEquals("06:16 AM", stringifyAtTimezone(prayerTimes.sunrise, zoneId)) 184 | assertEquals("01:09 PM", stringifyAtTimezone(prayerTimes.dhuhr, zoneId)) 185 | assertEquals("04:53 PM", stringifyAtTimezone(prayerTimes.asr, zoneId)) // original time 4:52pm 186 | assertEquals("07:52 PM", stringifyAtTimezone(prayerTimes.maghrib, zoneId)) 187 | assertEquals("09:19 PM", stringifyAtTimezone(prayerTimes.isha, zoneId)) // original time 9:18pm 188 | } 189 | 190 | @Test 191 | fun testEgyptian() { 192 | val date = DateComponents(2020, 1, 1) 193 | val parameters = CalculationMethod.EGYPTIAN.parameters 194 | val coordinates = Coordinates(latitude = 30.028703, longitude = 31.249528) 195 | 196 | val zoneId = "Africa/Cairo" 197 | val prayerTimes = PrayerTimes(coordinates, date, parameters) 198 | assertEquals("05:18 AM", stringifyAtTimezone(prayerTimes.fajr, zoneId)) 199 | assertEquals("06:51 AM", stringifyAtTimezone(prayerTimes.sunrise, zoneId)) 200 | assertEquals("11:59 AM", stringifyAtTimezone(prayerTimes.dhuhr, zoneId)) 201 | assertEquals("02:47 PM", stringifyAtTimezone(prayerTimes.asr, zoneId)) 202 | assertEquals("05:06 PM", stringifyAtTimezone(prayerTimes.maghrib, zoneId)) 203 | assertEquals("06:29 PM", stringifyAtTimezone(prayerTimes.isha, zoneId)) 204 | } 205 | 206 | @Test 207 | fun testTimeForPrayer() { 208 | val components = DateComponents(2016, 7, 1) 209 | val parameters = MUSLIM_WORLD_LEAGUE.parameters.copy( 210 | madhab = HANAFI, highLatitudeRule = TWILIGHT_ANGLE) 211 | val coordinates = Coordinates(59.9094, 10.7349) 212 | 213 | val p = PrayerTimes(coordinates, components, parameters) 214 | assertEquals(p.fajr, p.timeForPrayer(FAJR)) 215 | assertEquals(p.sunrise, p.timeForPrayer(SUNRISE)) 216 | assertEquals(p.dhuhr, p.timeForPrayer(DHUHR)) 217 | assertEquals(p.asr, p.timeForPrayer(ASR)) 218 | assertEquals(p.maghrib, p.timeForPrayer(MAGHRIB)) 219 | assertEquals(p.isha, p.timeForPrayer(ISHA)) 220 | 221 | assertNull(p.timeForPrayer(NONE)) 222 | } 223 | 224 | @Test 225 | fun testCurrentPrayer() { 226 | val components = DateComponents(2015, 9, 1) 227 | val parameters = KARACHI.parameters.copy(madhab = HANAFI, highLatitudeRule = TWILIGHT_ANGLE) 228 | val coordinates = Coordinates(33.720817, 73.090032) 229 | 230 | val p = PrayerTimes(coordinates, components, parameters) 231 | assertEquals(NONE, p.currentPrayer(addSeconds(p.fajr, -1))) 232 | assertEquals(FAJR, p.currentPrayer(p.fajr)) 233 | assertEquals(FAJR, p.currentPrayer(addSeconds(p.fajr, 1))) 234 | assertEquals(SUNRISE, p.currentPrayer(addSeconds(p.sunrise, 1))) 235 | assertEquals(DHUHR, p.currentPrayer(addSeconds(p.dhuhr, 1))) 236 | assertEquals(ASR, p.currentPrayer(addSeconds(p.asr, 1))) 237 | assertEquals(MAGHRIB, p.currentPrayer(addSeconds(p.maghrib, 1))) 238 | assertEquals(ISHA, p.currentPrayer(addSeconds(p.isha, 1))) 239 | } 240 | 241 | @Test 242 | fun testNextPrayer() { 243 | val components = DateComponents(2015, 9, 1) 244 | val parameters = KARACHI.parameters.copy(madhab = HANAFI, highLatitudeRule = TWILIGHT_ANGLE) 245 | val coordinates = Coordinates(33.720817, 73.090032) 246 | 247 | val p = PrayerTimes(coordinates, components, parameters) 248 | assertEquals(FAJR, p.nextPrayer(addSeconds(p.fajr, -1))) 249 | assertEquals(SUNRISE, p.nextPrayer(p.fajr)) 250 | assertEquals(SUNRISE, p.nextPrayer(addSeconds(p.fajr, 1))) 251 | assertEquals(DHUHR, p.nextPrayer(addSeconds(p.sunrise, 1))) 252 | assertEquals(ASR, p.nextPrayer(addSeconds(p.dhuhr, 1))) 253 | assertEquals(MAGHRIB, p.nextPrayer(addSeconds(p.asr, 1))) 254 | assertEquals(ISHA, p.nextPrayer(addSeconds(p.maghrib, 1))) 255 | assertEquals(NONE, p.nextPrayer(addSeconds(p.isha, 1))) 256 | } 257 | 258 | @Test 259 | fun testInvalidDate() { 260 | assertFailsWith { 261 | val date = DateComponents(0, 0, 0) 262 | PrayerTimes(Coordinates(33.720817, 73.090032), date, MUSLIM_WORLD_LEAGUE.parameters) 263 | } 264 | 265 | assertFailsWith { 266 | val date = DateComponents(-1, 99, 99) 267 | PrayerTimes(Coordinates(33.720817, 73.090032), date, MUSLIM_WORLD_LEAGUE.parameters) 268 | } 269 | } 270 | 271 | @Test 272 | fun testInvalidLocation() { 273 | assertFailsWith { 274 | val date = DateComponents(2019, 1, 1) 275 | PrayerTimes(Coordinates(999.0, 999.0), date, MUSLIM_WORLD_LEAGUE.parameters) 276 | } 277 | } 278 | 279 | @Test 280 | fun testExtremeLocation() { 281 | assertFailsWith { 282 | val date = DateComponents(2018, 1, 1) 283 | PrayerTimes(Coordinates(71.275009, -156.761368), date, MUSLIM_WORLD_LEAGUE.parameters) 284 | } 285 | 286 | val date = DateComponents(2018, 3, 1) 287 | val prayerTimes = PrayerTimes(Coordinates(71.275009, -156.761368), date, MUSLIM_WORLD_LEAGUE.parameters) 288 | assertNotNull(prayerTimes.fajr) 289 | } 290 | 291 | @Test 292 | fun testHighLatitudeRule() { 293 | val date = DateComponents(2020, 6, 15) 294 | val parameters = MUSLIM_WORLD_LEAGUE.parameters.copy(highLatitudeRule = MIDDLE_OF_THE_NIGHT) 295 | val coordinates = Coordinates(latitude = 55.983226, longitude = -3.216649) 296 | 297 | val zoneId = "Europe/London" 298 | val prayerTimes = PrayerTimes(coordinates, date, parameters) 299 | assertEquals("01:14 AM", stringifyAtTimezone(prayerTimes.fajr, zoneId)) 300 | assertEquals("04:26 AM", stringifyAtTimezone(prayerTimes.sunrise, zoneId)) 301 | assertEquals("01:14 PM", stringifyAtTimezone(prayerTimes.dhuhr, zoneId)) 302 | assertEquals("05:46 PM", stringifyAtTimezone(prayerTimes.asr, zoneId)) 303 | assertEquals("10:01 PM", stringifyAtTimezone(prayerTimes.maghrib, zoneId)) 304 | assertEquals("01:14 AM", stringifyAtTimezone(prayerTimes.isha, zoneId)) 305 | 306 | val seventhParams = parameters.copy(highLatitudeRule = SEVENTH_OF_THE_NIGHT) 307 | val seventhPrayerTimes = PrayerTimes(coordinates, date, seventhParams) 308 | assertEquals("03:31 AM", stringifyAtTimezone(seventhPrayerTimes.fajr, zoneId)) 309 | assertEquals("04:26 AM", stringifyAtTimezone(seventhPrayerTimes.sunrise, zoneId)) 310 | assertEquals("01:14 PM", stringifyAtTimezone(seventhPrayerTimes.dhuhr, zoneId)) 311 | assertEquals("05:46 PM", stringifyAtTimezone(seventhPrayerTimes.asr, zoneId)) 312 | assertEquals("10:01 PM", stringifyAtTimezone(seventhPrayerTimes.maghrib, zoneId)) 313 | assertEquals("10:56 PM", stringifyAtTimezone(seventhPrayerTimes.isha, zoneId)) 314 | 315 | val twilightParams = parameters.copy(highLatitudeRule = TWILIGHT_ANGLE) 316 | val twilightPrayerTimes = PrayerTimes(coordinates, date, twilightParams) 317 | assertEquals("02:31 AM", stringifyAtTimezone(twilightPrayerTimes.fajr, zoneId)) 318 | assertEquals("04:26 AM", stringifyAtTimezone(twilightPrayerTimes.sunrise, zoneId)) 319 | assertEquals("01:14 PM", stringifyAtTimezone(twilightPrayerTimes.dhuhr, zoneId)) 320 | assertEquals("05:46 PM", stringifyAtTimezone(twilightPrayerTimes.asr, zoneId)) 321 | assertEquals("10:01 PM", stringifyAtTimezone(twilightPrayerTimes.maghrib, zoneId)) 322 | assertEquals("11:50 PM", stringifyAtTimezone(twilightPrayerTimes.isha, zoneId)) 323 | 324 | val autoHighLatitudeRule = parameters.copy(highLatitudeRule = null) 325 | val autoPrayerTimes = PrayerTimes(coordinates, date, autoHighLatitudeRule) 326 | assertEquals(seventhPrayerTimes.fajr, autoPrayerTimes.fajr) 327 | assertEquals(seventhPrayerTimes.sunrise, autoPrayerTimes.sunrise) 328 | assertEquals(seventhPrayerTimes.dhuhr, autoPrayerTimes.dhuhr) 329 | assertEquals(seventhPrayerTimes.asr, autoPrayerTimes.asr) 330 | assertEquals(seventhPrayerTimes.maghrib, autoPrayerTimes.maghrib) 331 | assertEquals(seventhPrayerTimes.isha, autoPrayerTimes.isha) 332 | } 333 | 334 | @Test 335 | fun testRecommendedHighLatitudeRule() { 336 | val coords1 = Coordinates(latitude = 45.983226, longitude = -3.216649) 337 | assertEquals(MIDDLE_OF_THE_NIGHT, HighLatitudeRule.recommendedFor(coords1)) 338 | 339 | val coords2 = Coordinates(latitude = 48.983226, longitude = -3.216649) 340 | assertEquals(SEVENTH_OF_THE_NIGHT, HighLatitudeRule.recommendedFor(coords2)) 341 | } 342 | 343 | @Test 344 | fun testShafaqGeneral() { 345 | val parameters = MOON_SIGHTING_COMMITTEE.parameters.copy(shafaq = Shafaq.GENERAL, madhab = HANAFI) 346 | val coordinates = Coordinates(latitude = 43.494, longitude = -79.844) 347 | 348 | val zoneId = "America/New_York" 349 | 350 | val date = DateComponents(2021, 1, 1) 351 | val prayerTimes = PrayerTimes(coordinates, date, parameters) 352 | assertEquals("06:16 AM", stringifyAtTimezone(prayerTimes.fajr, zoneId)) 353 | assertEquals("07:52 AM", stringifyAtTimezone(prayerTimes.sunrise, zoneId)) 354 | assertEquals("12:28 PM", stringifyAtTimezone(prayerTimes.dhuhr, zoneId)) 355 | assertEquals("03:12 PM", stringifyAtTimezone(prayerTimes.asr, zoneId)) 356 | assertEquals("04:57 PM", stringifyAtTimezone(prayerTimes.maghrib, zoneId)) 357 | assertEquals("06:27 PM", stringifyAtTimezone(prayerTimes.isha, zoneId)) 358 | 359 | val secondDate = DateComponents(2021, 4, 1) 360 | val secondPrayerTimes = PrayerTimes(coordinates, secondDate, parameters) 361 | assertEquals("05:28 AM", stringifyAtTimezone(secondPrayerTimes.fajr, zoneId)) 362 | assertEquals("07:01 AM", stringifyAtTimezone(secondPrayerTimes.sunrise, zoneId)) 363 | assertEquals("01:28 PM", stringifyAtTimezone(secondPrayerTimes.dhuhr, zoneId)) 364 | assertEquals("05:53 PM", stringifyAtTimezone(secondPrayerTimes.asr, zoneId)) 365 | assertEquals("07:49 PM", stringifyAtTimezone(secondPrayerTimes.maghrib, zoneId)) 366 | assertEquals("09:01 PM", stringifyAtTimezone(secondPrayerTimes.isha, zoneId)) 367 | 368 | val thirdDate = DateComponents(2021, 7, 1) 369 | val thirdPrayerTimes = PrayerTimes(coordinates, thirdDate, parameters) 370 | assertEquals("03:52 AM", stringifyAtTimezone(thirdPrayerTimes.fajr, zoneId)) 371 | assertEquals("05:42 AM", stringifyAtTimezone(thirdPrayerTimes.sunrise, zoneId)) 372 | assertEquals("01:28 PM", stringifyAtTimezone(thirdPrayerTimes.dhuhr, zoneId)) 373 | assertEquals("06:42 PM", stringifyAtTimezone(thirdPrayerTimes.asr, zoneId)) 374 | assertEquals("09:07 PM", stringifyAtTimezone(thirdPrayerTimes.maghrib, zoneId)) 375 | assertEquals("10:22 PM", stringifyAtTimezone(thirdPrayerTimes.isha, zoneId)) 376 | 377 | val fourthDate = DateComponents(2021, 11, 1) 378 | val fourthPrayerTimes = PrayerTimes(coordinates, fourthDate, parameters) 379 | assertEquals("06:22 AM", stringifyAtTimezone(fourthPrayerTimes.fajr, zoneId)) 380 | assertEquals("07:55 AM", stringifyAtTimezone(fourthPrayerTimes.sunrise, zoneId)) 381 | assertEquals("01:08 PM", stringifyAtTimezone(fourthPrayerTimes.dhuhr, zoneId)) 382 | assertEquals("04:26 PM", stringifyAtTimezone(fourthPrayerTimes.asr, zoneId)) 383 | assertEquals("06:13 PM", stringifyAtTimezone(fourthPrayerTimes.maghrib, zoneId)) 384 | assertEquals("07:35 PM", stringifyAtTimezone(fourthPrayerTimes.isha, zoneId)) 385 | } 386 | 387 | @Test 388 | fun testShafaqAhmer() { 389 | val parameters = MOON_SIGHTING_COMMITTEE.parameters.copy(shafaq = Shafaq.AHMER) 390 | val coordinates = Coordinates(latitude = 43.494, longitude = -79.844) 391 | 392 | val zoneId = "America/New_York" 393 | 394 | val date = DateComponents(2021, 1, 1) 395 | val prayerTimes = PrayerTimes(coordinates, date, parameters) 396 | assertEquals("06:16 AM", stringifyAtTimezone(prayerTimes.fajr, zoneId)) 397 | assertEquals("07:52 AM", stringifyAtTimezone(prayerTimes.sunrise, zoneId)) 398 | assertEquals("12:28 PM", stringifyAtTimezone(prayerTimes.dhuhr, zoneId)) 399 | assertEquals("02:37 PM", stringifyAtTimezone(prayerTimes.asr, zoneId)) 400 | assertEquals("04:57 PM", stringifyAtTimezone(prayerTimes.maghrib, zoneId)) 401 | assertEquals("06:07 PM", stringifyAtTimezone(prayerTimes.isha, zoneId)) // value from source is 6:08 PM 402 | 403 | val secondDate = DateComponents(2021, 4, 1) 404 | val secondPrayerTimes = PrayerTimes(coordinates, secondDate, parameters) 405 | assertEquals("05:28 AM", stringifyAtTimezone(secondPrayerTimes.fajr, zoneId)) 406 | assertEquals("07:01 AM", stringifyAtTimezone(secondPrayerTimes.sunrise, zoneId)) 407 | assertEquals("01:28 PM", stringifyAtTimezone(secondPrayerTimes.dhuhr, zoneId)) 408 | assertEquals("04:59 PM", stringifyAtTimezone(secondPrayerTimes.asr, zoneId)) 409 | assertEquals("07:49 PM", stringifyAtTimezone(secondPrayerTimes.maghrib, zoneId)) 410 | assertEquals("08:45 PM", stringifyAtTimezone(secondPrayerTimes.isha, zoneId)) 411 | 412 | val thirdDate = DateComponents(2021, 7, 1) 413 | val thirdPrayerTimes = PrayerTimes(coordinates, thirdDate, parameters) 414 | assertEquals("03:52 AM", stringifyAtTimezone(thirdPrayerTimes.fajr, zoneId)) 415 | assertEquals("05:42 AM", stringifyAtTimezone(thirdPrayerTimes.sunrise, zoneId)) 416 | assertEquals("01:28 PM", stringifyAtTimezone(thirdPrayerTimes.dhuhr, zoneId)) 417 | assertEquals("05:29 PM", stringifyAtTimezone(thirdPrayerTimes.asr, zoneId)) 418 | assertEquals("09:07 PM", stringifyAtTimezone(thirdPrayerTimes.maghrib, zoneId)) 419 | assertEquals("10:19 PM", stringifyAtTimezone(thirdPrayerTimes.isha, zoneId)) 420 | 421 | val fourthDate = DateComponents(2021, 11, 1) 422 | val fourthPrayerTimes = PrayerTimes(coordinates, fourthDate, parameters) 423 | assertEquals("06:22 AM", stringifyAtTimezone(fourthPrayerTimes.fajr, zoneId)) 424 | assertEquals("07:55 AM", stringifyAtTimezone(fourthPrayerTimes.sunrise, zoneId)) 425 | assertEquals("01:08 PM", stringifyAtTimezone(fourthPrayerTimes.dhuhr, zoneId)) 426 | assertEquals("03:45 PM", stringifyAtTimezone(fourthPrayerTimes.asr, zoneId)) 427 | assertEquals("06:13 PM", stringifyAtTimezone(fourthPrayerTimes.maghrib, zoneId)) 428 | assertEquals("07:15 PM", stringifyAtTimezone(fourthPrayerTimes.isha, zoneId)) 429 | } 430 | 431 | @Test 432 | fun testShafaqAbyad() { 433 | val parameters = MOON_SIGHTING_COMMITTEE.parameters.copy(shafaq = Shafaq.ABYAD, madhab = HANAFI) 434 | val coordinates = Coordinates(latitude = 43.494, longitude = -79.844) 435 | 436 | val zoneId = "America/New_York" 437 | 438 | val date = DateComponents(2021, 1, 1) 439 | val prayerTimes = PrayerTimes(coordinates, date, parameters) 440 | assertEquals("06:16 AM", stringifyAtTimezone(prayerTimes.fajr, zoneId)) 441 | assertEquals("07:52 AM", stringifyAtTimezone(prayerTimes.sunrise, zoneId)) 442 | assertEquals("12:28 PM", stringifyAtTimezone(prayerTimes.dhuhr, zoneId)) 443 | assertEquals("03:12 PM", stringifyAtTimezone(prayerTimes.asr, zoneId)) 444 | assertEquals("04:57 PM", stringifyAtTimezone(prayerTimes.maghrib, zoneId)) 445 | assertEquals("06:28 PM", stringifyAtTimezone(prayerTimes.isha, zoneId)) 446 | 447 | val secondDate = DateComponents(2021, 4, 1) 448 | val secondPrayerTimes = PrayerTimes(coordinates, secondDate, parameters) 449 | assertEquals("05:28 AM", stringifyAtTimezone(secondPrayerTimes.fajr, zoneId)) 450 | assertEquals("07:01 AM", stringifyAtTimezone(secondPrayerTimes.sunrise, zoneId)) 451 | assertEquals("01:28 PM", stringifyAtTimezone(secondPrayerTimes.dhuhr, zoneId)) 452 | assertEquals("05:53 PM", stringifyAtTimezone(secondPrayerTimes.asr, zoneId)) 453 | assertEquals("07:49 PM", stringifyAtTimezone(secondPrayerTimes.maghrib, zoneId)) 454 | assertEquals("09:12 PM", stringifyAtTimezone(secondPrayerTimes.isha, zoneId)) 455 | 456 | val thirdDate = DateComponents(2021, 7, 1) 457 | val thirdPrayerTimes = PrayerTimes(coordinates, thirdDate, parameters) 458 | assertEquals("03:52 AM", stringifyAtTimezone(thirdPrayerTimes.fajr, zoneId)) 459 | assertEquals("05:42 AM", stringifyAtTimezone(thirdPrayerTimes.sunrise, zoneId)) 460 | assertEquals("01:28 PM", stringifyAtTimezone(thirdPrayerTimes.dhuhr, zoneId)) 461 | assertEquals("06:42 PM", stringifyAtTimezone(thirdPrayerTimes.asr, zoneId)) 462 | assertEquals("09:07 PM", stringifyAtTimezone(thirdPrayerTimes.maghrib, zoneId)) 463 | assertEquals("11:17 PM", stringifyAtTimezone(thirdPrayerTimes.isha, zoneId)) 464 | 465 | val fourthDate = DateComponents(2021, 11, 1) 466 | val fourthPrayerTimes = PrayerTimes(coordinates, fourthDate, parameters) 467 | assertEquals("06:22 AM", stringifyAtTimezone(fourthPrayerTimes.fajr, zoneId)) 468 | assertEquals("07:55 AM", stringifyAtTimezone(fourthPrayerTimes.sunrise, zoneId)) 469 | assertEquals("01:08 PM", stringifyAtTimezone(fourthPrayerTimes.dhuhr, zoneId)) 470 | assertEquals("04:26 PM", stringifyAtTimezone(fourthPrayerTimes.asr, zoneId)) 471 | assertEquals("06:13 PM", stringifyAtTimezone(fourthPrayerTimes.maghrib, zoneId)) 472 | assertEquals("07:37 PM", stringifyAtTimezone(fourthPrayerTimes.isha, zoneId)) 473 | } 474 | } 475 | -------------------------------------------------------------------------------- /kotlin-js-store/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@isaacs/cliui@^8.0.2": 6 | version "8.0.2" 7 | resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" 8 | integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== 9 | dependencies: 10 | string-width "^5.1.2" 11 | string-width-cjs "npm:string-width@^4.2.0" 12 | strip-ansi "^7.0.1" 13 | strip-ansi-cjs "npm:strip-ansi@^6.0.1" 14 | wrap-ansi "^8.1.0" 15 | wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" 16 | 17 | "@js-joda/core@3.2.0": 18 | version "3.2.0" 19 | resolved "https://registry.yarnpkg.com/@js-joda/core/-/core-3.2.0.tgz#3e61e21b7b2b8a6be746df1335cf91d70db2a273" 20 | integrity sha512-PMqgJ0sw5B7FKb2d5bWYIoxjri+QlW/Pys7+Rw82jSH0QN3rB05jZ/VrrsUdh1w4+i2kw9JOejXGq/KhDOX7Kg== 21 | 22 | "@js-joda/timezone@2.3.0": 23 | version "2.3.0" 24 | resolved "https://registry.yarnpkg.com/@js-joda/timezone/-/timezone-2.3.0.tgz#72878f6dc8afef20c29906e5d8d946f91618a2c3" 25 | integrity sha512-DHXdNs0SydSqC5f0oRJPpTcNfnpRojgBqMCFupQFv6WgeZAjU3DBx+A7JtaGPP3dHrP2Odi2N8Vf+uAm/8ynCQ== 26 | 27 | "@pkgjs/parseargs@^0.11.0": 28 | version "0.11.0" 29 | resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" 30 | integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== 31 | 32 | ansi-regex@^5.0.1: 33 | version "5.0.1" 34 | resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" 35 | integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== 36 | 37 | ansi-regex@^6.0.1: 38 | version "6.2.2" 39 | resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.2.2.tgz#60216eea464d864597ce2832000738a0589650c1" 40 | integrity sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg== 41 | 42 | ansi-styles@^4.0.0, ansi-styles@^4.1.0: 43 | version "4.3.0" 44 | resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" 45 | integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== 46 | dependencies: 47 | color-convert "^2.0.1" 48 | 49 | ansi-styles@^6.1.0: 50 | version "6.2.3" 51 | resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.3.tgz#c044d5dcc521a076413472597a1acb1f103c4041" 52 | integrity sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg== 53 | 54 | argparse@^2.0.1: 55 | version "2.0.1" 56 | resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" 57 | integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== 58 | 59 | balanced-match@^1.0.0: 60 | version "1.0.2" 61 | resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" 62 | integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== 63 | 64 | brace-expansion@^2.0.1: 65 | version "2.0.1" 66 | resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" 67 | integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== 68 | dependencies: 69 | balanced-match "^1.0.0" 70 | 71 | browser-stdout@^1.3.1: 72 | version "1.3.1" 73 | resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" 74 | integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== 75 | 76 | buffer-from@^1.0.0: 77 | version "1.1.2" 78 | resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" 79 | integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== 80 | 81 | camelcase@^6.0.0: 82 | version "6.3.0" 83 | resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" 84 | integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== 85 | 86 | chalk@^4.1.0: 87 | version "4.1.2" 88 | resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" 89 | integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== 90 | dependencies: 91 | ansi-styles "^4.1.0" 92 | supports-color "^7.1.0" 93 | 94 | chokidar@^4.0.1: 95 | version "4.0.3" 96 | resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.3.tgz#7be37a4c03c9aee1ecfe862a4a23b2c70c205d30" 97 | integrity sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA== 98 | dependencies: 99 | readdirp "^4.0.1" 100 | 101 | cliui@^8.0.1: 102 | version "8.0.1" 103 | resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" 104 | integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== 105 | dependencies: 106 | string-width "^4.2.0" 107 | strip-ansi "^6.0.1" 108 | wrap-ansi "^7.0.0" 109 | 110 | color-convert@^2.0.1: 111 | version "2.0.1" 112 | resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" 113 | integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== 114 | dependencies: 115 | color-name "~1.1.4" 116 | 117 | color-name@~1.1.4: 118 | version "1.1.4" 119 | resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" 120 | integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== 121 | 122 | cross-spawn@^7.0.6: 123 | version "7.0.6" 124 | resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" 125 | integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== 126 | dependencies: 127 | path-key "^3.1.0" 128 | shebang-command "^2.0.0" 129 | which "^2.0.1" 130 | 131 | debug@^4.3.5: 132 | version "4.3.7" 133 | resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" 134 | integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== 135 | dependencies: 136 | ms "^2.1.3" 137 | 138 | decamelize@^4.0.0: 139 | version "4.0.0" 140 | resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837" 141 | integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== 142 | 143 | diff@^7.0.0: 144 | version "7.0.0" 145 | resolved "https://registry.yarnpkg.com/diff/-/diff-7.0.0.tgz#3fb34d387cd76d803f6eebea67b921dab0182a9a" 146 | integrity sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw== 147 | 148 | eastasianwidth@^0.2.0: 149 | version "0.2.0" 150 | resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" 151 | integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== 152 | 153 | emoji-regex@^8.0.0: 154 | version "8.0.0" 155 | resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" 156 | integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== 157 | 158 | emoji-regex@^9.2.2: 159 | version "9.2.2" 160 | resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" 161 | integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== 162 | 163 | escalade@^3.1.1: 164 | version "3.1.1" 165 | resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" 166 | integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== 167 | 168 | escape-string-regexp@^4.0.0: 169 | version "4.0.0" 170 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" 171 | integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== 172 | 173 | find-up@^5.0.0: 174 | version "5.0.0" 175 | resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" 176 | integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== 177 | dependencies: 178 | locate-path "^6.0.0" 179 | path-exists "^4.0.0" 180 | 181 | flat@^5.0.2: 182 | version "5.0.2" 183 | resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" 184 | integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== 185 | 186 | foreground-child@^3.1.0: 187 | version "3.3.1" 188 | resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.1.tgz#32e8e9ed1b68a3497befb9ac2b6adf92a638576f" 189 | integrity sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw== 190 | dependencies: 191 | cross-spawn "^7.0.6" 192 | signal-exit "^4.0.1" 193 | 194 | format-util@^1.0.5: 195 | version "1.0.5" 196 | resolved "https://registry.yarnpkg.com/format-util/-/format-util-1.0.5.tgz#1ffb450c8a03e7bccffe40643180918cc297d271" 197 | integrity sha512-varLbTj0e0yVyRpqQhuWV+8hlePAgaoFRhNFj50BNjEIrw1/DphHSObtqwskVCPWNgzwPoQrZAbfa/SBiicNeg== 198 | 199 | get-caller-file@^2.0.5: 200 | version "2.0.5" 201 | resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" 202 | integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== 203 | 204 | glob@^10.4.5: 205 | version "10.4.5" 206 | resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956" 207 | integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg== 208 | dependencies: 209 | foreground-child "^3.1.0" 210 | jackspeak "^3.1.2" 211 | minimatch "^9.0.4" 212 | minipass "^7.1.2" 213 | package-json-from-dist "^1.0.0" 214 | path-scurry "^1.11.1" 215 | 216 | has-flag@^4.0.0: 217 | version "4.0.0" 218 | resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" 219 | integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== 220 | 221 | he@^1.2.0: 222 | version "1.2.0" 223 | resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" 224 | integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== 225 | 226 | is-fullwidth-code-point@^3.0.0: 227 | version "3.0.0" 228 | resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" 229 | integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== 230 | 231 | is-plain-obj@^2.1.0: 232 | version "2.1.0" 233 | resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" 234 | integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== 235 | 236 | is-unicode-supported@^0.1.0: 237 | version "0.1.0" 238 | resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" 239 | integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== 240 | 241 | isexe@^2.0.0: 242 | version "2.0.0" 243 | resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" 244 | integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== 245 | 246 | jackspeak@^3.1.2: 247 | version "3.4.3" 248 | resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.3.tgz#8833a9d89ab4acde6188942bd1c53b6390ed5a8a" 249 | integrity sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw== 250 | dependencies: 251 | "@isaacs/cliui" "^8.0.2" 252 | optionalDependencies: 253 | "@pkgjs/parseargs" "^0.11.0" 254 | 255 | js-yaml@^4.1.0: 256 | version "4.1.0" 257 | resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" 258 | integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== 259 | dependencies: 260 | argparse "^2.0.1" 261 | 262 | kotlin-web-helpers@2.1.0: 263 | version "2.1.0" 264 | resolved "https://registry.yarnpkg.com/kotlin-web-helpers/-/kotlin-web-helpers-2.1.0.tgz#6cd4b0f0dc3baea163929c8638155b8d19c55a74" 265 | integrity sha512-NAJhiNB84tnvJ5EQx7iER3GWw7rsTZkX9HVHZpe7E3dDBD/dhTzqgSwNU3MfQjniy2rB04bP24WM9Z32ntUWRg== 266 | dependencies: 267 | format-util "^1.0.5" 268 | 269 | locate-path@^6.0.0: 270 | version "6.0.0" 271 | resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" 272 | integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== 273 | dependencies: 274 | p-locate "^5.0.0" 275 | 276 | log-symbols@^4.1.0: 277 | version "4.1.0" 278 | resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" 279 | integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== 280 | dependencies: 281 | chalk "^4.1.0" 282 | is-unicode-supported "^0.1.0" 283 | 284 | lru-cache@^10.2.0: 285 | version "10.4.3" 286 | resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" 287 | integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== 288 | 289 | minimatch@^9.0.4, minimatch@^9.0.5: 290 | version "9.0.5" 291 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" 292 | integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== 293 | dependencies: 294 | brace-expansion "^2.0.1" 295 | 296 | "minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.1.2: 297 | version "7.1.2" 298 | resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" 299 | integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== 300 | 301 | mocha@11.7.1: 302 | version "11.7.1" 303 | resolved "https://registry.yarnpkg.com/mocha/-/mocha-11.7.1.tgz#91948fecd624fb4bd154ed260b7e1ad3910d7c7a" 304 | integrity sha512-5EK+Cty6KheMS/YLPPMJC64g5V61gIR25KsRItHw6x4hEKT6Njp1n9LOlH4gpevuwMVS66SXaBBpg+RWZkza4A== 305 | dependencies: 306 | browser-stdout "^1.3.1" 307 | chokidar "^4.0.1" 308 | debug "^4.3.5" 309 | diff "^7.0.0" 310 | escape-string-regexp "^4.0.0" 311 | find-up "^5.0.0" 312 | glob "^10.4.5" 313 | he "^1.2.0" 314 | js-yaml "^4.1.0" 315 | log-symbols "^4.1.0" 316 | minimatch "^9.0.5" 317 | ms "^2.1.3" 318 | picocolors "^1.1.1" 319 | serialize-javascript "^6.0.2" 320 | strip-json-comments "^3.1.1" 321 | supports-color "^8.1.1" 322 | workerpool "^9.2.0" 323 | yargs "^17.7.2" 324 | yargs-parser "^21.1.1" 325 | yargs-unparser "^2.0.0" 326 | 327 | ms@^2.1.3: 328 | version "2.1.3" 329 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" 330 | integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== 331 | 332 | p-limit@^3.0.2: 333 | version "3.1.0" 334 | resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" 335 | integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== 336 | dependencies: 337 | yocto-queue "^0.1.0" 338 | 339 | p-locate@^5.0.0: 340 | version "5.0.0" 341 | resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" 342 | integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== 343 | dependencies: 344 | p-limit "^3.0.2" 345 | 346 | package-json-from-dist@^1.0.0: 347 | version "1.0.1" 348 | resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz#4f1471a010827a86f94cfd9b0727e36d267de505" 349 | integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw== 350 | 351 | path-exists@^4.0.0: 352 | version "4.0.0" 353 | resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" 354 | integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== 355 | 356 | path-key@^3.1.0: 357 | version "3.1.1" 358 | resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" 359 | integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== 360 | 361 | path-scurry@^1.11.1: 362 | version "1.11.1" 363 | resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2" 364 | integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== 365 | dependencies: 366 | lru-cache "^10.2.0" 367 | minipass "^5.0.0 || ^6.0.2 || ^7.0.0" 368 | 369 | picocolors@^1.1.1: 370 | version "1.1.1" 371 | resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" 372 | integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== 373 | 374 | randombytes@^2.1.0: 375 | version "2.1.0" 376 | resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" 377 | integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== 378 | dependencies: 379 | safe-buffer "^5.1.0" 380 | 381 | readdirp@^4.0.1: 382 | version "4.1.2" 383 | resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.1.2.tgz#eb85801435fbf2a7ee58f19e0921b068fc69948d" 384 | integrity sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg== 385 | 386 | require-directory@^2.1.1: 387 | version "2.1.1" 388 | resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" 389 | integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== 390 | 391 | safe-buffer@^5.1.0: 392 | version "5.2.1" 393 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" 394 | integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== 395 | 396 | serialize-javascript@^6.0.2: 397 | version "6.0.2" 398 | resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" 399 | integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== 400 | dependencies: 401 | randombytes "^2.1.0" 402 | 403 | shebang-command@^2.0.0: 404 | version "2.0.0" 405 | resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" 406 | integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== 407 | dependencies: 408 | shebang-regex "^3.0.0" 409 | 410 | shebang-regex@^3.0.0: 411 | version "3.0.0" 412 | resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" 413 | integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== 414 | 415 | signal-exit@^4.0.1: 416 | version "4.1.0" 417 | resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" 418 | integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== 419 | 420 | source-map-support@0.5.21: 421 | version "0.5.21" 422 | resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" 423 | integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== 424 | dependencies: 425 | buffer-from "^1.0.0" 426 | source-map "^0.6.0" 427 | 428 | source-map@^0.6.0: 429 | version "0.6.1" 430 | resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" 431 | integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== 432 | 433 | "string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: 434 | version "4.2.3" 435 | resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" 436 | integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== 437 | dependencies: 438 | emoji-regex "^8.0.0" 439 | is-fullwidth-code-point "^3.0.0" 440 | strip-ansi "^6.0.1" 441 | 442 | string-width@^5.0.1, string-width@^5.1.2: 443 | version "5.1.2" 444 | resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" 445 | integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== 446 | dependencies: 447 | eastasianwidth "^0.2.0" 448 | emoji-regex "^9.2.2" 449 | strip-ansi "^7.0.1" 450 | 451 | "strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: 452 | version "6.0.1" 453 | resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" 454 | integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== 455 | dependencies: 456 | ansi-regex "^5.0.1" 457 | 458 | strip-ansi@^7.0.1: 459 | version "7.1.2" 460 | resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.2.tgz#132875abde678c7ea8d691533f2e7e22bb744dba" 461 | integrity sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA== 462 | dependencies: 463 | ansi-regex "^6.0.1" 464 | 465 | strip-json-comments@^3.1.1: 466 | version "3.1.1" 467 | resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" 468 | integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== 469 | 470 | supports-color@^7.1.0: 471 | version "7.2.0" 472 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" 473 | integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== 474 | dependencies: 475 | has-flag "^4.0.0" 476 | 477 | supports-color@^8.1.1: 478 | version "8.1.1" 479 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" 480 | integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== 481 | dependencies: 482 | has-flag "^4.0.0" 483 | 484 | which@^2.0.1: 485 | version "2.0.2" 486 | resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" 487 | integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== 488 | dependencies: 489 | isexe "^2.0.0" 490 | 491 | workerpool@^9.2.0: 492 | version "9.3.4" 493 | resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-9.3.4.tgz#f6c92395b2141afd78e2a889e80cb338fe9fca41" 494 | integrity sha512-TmPRQYYSAnnDiEB0P/Ytip7bFGvqnSU6I2BcuSw7Hx+JSg/DsUi5ebYfc8GYaSdpuvOcEs6dXxPurOYpe9QFwg== 495 | 496 | "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: 497 | version "7.0.0" 498 | resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" 499 | integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== 500 | dependencies: 501 | ansi-styles "^4.0.0" 502 | string-width "^4.1.0" 503 | strip-ansi "^6.0.0" 504 | 505 | wrap-ansi@^8.1.0: 506 | version "8.1.0" 507 | resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" 508 | integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== 509 | dependencies: 510 | ansi-styles "^6.1.0" 511 | string-width "^5.0.1" 512 | strip-ansi "^7.0.1" 513 | 514 | y18n@^5.0.5: 515 | version "5.0.8" 516 | resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" 517 | integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== 518 | 519 | yargs-parser@^21.1.1: 520 | version "21.1.1" 521 | resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" 522 | integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== 523 | 524 | yargs-unparser@^2.0.0: 525 | version "2.0.0" 526 | resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb" 527 | integrity sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA== 528 | dependencies: 529 | camelcase "^6.0.0" 530 | decamelize "^4.0.0" 531 | flat "^5.0.2" 532 | is-plain-obj "^2.1.0" 533 | 534 | yargs@^17.7.2: 535 | version "17.7.2" 536 | resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" 537 | integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== 538 | dependencies: 539 | cliui "^8.0.1" 540 | escalade "^3.1.1" 541 | get-caller-file "^2.0.5" 542 | require-directory "^2.1.1" 543 | string-width "^4.2.3" 544 | y18n "^5.0.5" 545 | yargs-parser "^21.1.1" 546 | 547 | yocto-queue@^0.1.0: 548 | version "0.1.0" 549 | resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" 550 | integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== 551 | --------------------------------------------------------------------------------