mirror of
https://github.com/breezy-weather/breezy-weather.git
synced 2025-10-18 23:43:40 +00:00
Unit tests
This commit is contained in:
parent
89a7806492
commit
24ecafe734
23 changed files with 685 additions and 321 deletions
|
@ -91,7 +91,7 @@ object UnitUtils {
|
|||
locale = context.currentLocale,
|
||||
showSign = showSign,
|
||||
useNumberFormatter = SettingsManager.Companion.getInstance(context).useNumberFormatter,
|
||||
useMeasureFormat = SettingsManager.Companion.getInstance(context).useMeasureFormat
|
||||
useNumberFormat = SettingsManager.Companion.getInstance(context).useMeasureFormat
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -106,7 +106,7 @@ object UnitUtils {
|
|||
locale = context.currentLocale,
|
||||
showSign = showSign,
|
||||
useNumberFormatter = SettingsManager.Companion.getInstance(context).useNumberFormatter,
|
||||
useMeasureFormat = SettingsManager.Companion.getInstance(context).useMeasureFormat
|
||||
useNumberFormat = SettingsManager.Companion.getInstance(context).useMeasureFormat
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -45,9 +45,13 @@ import org.breezyweather.common.extensions.CLOUD_COVER_FEW
|
|||
import org.breezyweather.common.extensions.ensurePositive
|
||||
import org.breezyweather.common.extensions.getIsoFormattedDate
|
||||
import org.breezyweather.common.extensions.toCalendarWithTimeZone
|
||||
import org.breezyweather.domain.weather.index.PollutantIndex
|
||||
import org.breezyweather.domain.weather.model.validate
|
||||
import org.breezyweather.ui.theme.weatherView.WeatherViewController
|
||||
import org.breezyweather.unit.computing.computeApparentTemperature
|
||||
import org.breezyweather.unit.computing.computeDewPoint
|
||||
import org.breezyweather.unit.computing.computeHumidex
|
||||
import org.breezyweather.unit.computing.computeRelativeHumidity
|
||||
import org.breezyweather.unit.computing.computeWindChillTemperature
|
||||
import org.breezyweather.unit.distance.Distance
|
||||
import org.breezyweather.unit.distance.Distance.Companion.meters
|
||||
import org.breezyweather.unit.duration.toValidDailyOrNull
|
||||
|
@ -58,14 +62,11 @@ import org.breezyweather.unit.precipitation.Precipitation.Companion.millimeters
|
|||
import org.breezyweather.unit.pressure.Pressure
|
||||
import org.breezyweather.unit.pressure.Pressure.Companion.pascals
|
||||
import org.breezyweather.unit.ratio.Ratio
|
||||
import org.breezyweather.unit.ratio.Ratio.Companion.fraction
|
||||
import org.breezyweather.unit.ratio.Ratio.Companion.percent
|
||||
import org.breezyweather.unit.ratio.Ratio.Companion.permille
|
||||
import org.breezyweather.unit.speed.Speed
|
||||
import org.breezyweather.unit.speed.Speed.Companion.kilometersPerHour
|
||||
import org.breezyweather.unit.speed.Speed.Companion.metersPerSecond
|
||||
import org.breezyweather.unit.temperature.Temperature
|
||||
import org.breezyweather.unit.temperature.Temperature.Companion.celsius
|
||||
import org.breezyweather.unit.temperature.Temperature.Companion.deciCelsius
|
||||
import org.shredzone.commons.suncalc.MoonIllumination
|
||||
import org.shredzone.commons.suncalc.MoonTimes
|
||||
|
@ -74,10 +75,6 @@ import java.util.Calendar
|
|||
import java.util.Date
|
||||
import java.util.TimeZone
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.exp
|
||||
import kotlin.math.ln
|
||||
import kotlin.math.log10
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.days
|
||||
|
@ -274,121 +271,6 @@ internal fun computeMissingHourlyData(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute relative humidity from temperature and dew point
|
||||
* Uses Magnus approximation with Arden Buck best variable set
|
||||
* TODO: Unit test
|
||||
*/
|
||||
private fun computeRelativeHumidity(
|
||||
temperature: Temperature?,
|
||||
dewPoint: Temperature?,
|
||||
): Ratio? {
|
||||
if (temperature == null || dewPoint == null) return null
|
||||
|
||||
val b = if (temperature < 0.celsius) 17.966 else 17.368
|
||||
val c = if (temperature < 0.celsius) 227.15 else 238.88 // °C
|
||||
|
||||
return (
|
||||
exp((b * dewPoint.inCelsius).div(c + dewPoint.inCelsius)) /
|
||||
exp((b * temperature.inCelsius).div(c + temperature.inCelsius))
|
||||
).fraction
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute dew point from temperature and relative humidity
|
||||
* Uses Magnus approximation with Arden Buck best variable set
|
||||
* TODO: Unit test
|
||||
*
|
||||
* @param temperature
|
||||
* @param relativeHumidity
|
||||
*/
|
||||
private fun computeDewPoint(
|
||||
temperature: Temperature?,
|
||||
relativeHumidity: Ratio?,
|
||||
): Temperature? {
|
||||
if (temperature == null || relativeHumidity == null) return null
|
||||
|
||||
val b = if (temperature < 0.celsius) 17.966 else 17.368
|
||||
val c = if (temperature < 0.celsius) 227.15 else 238.88 // °C
|
||||
|
||||
val magnus = ln(relativeHumidity.inFraction) + (b * temperature.inCelsius) / (c + temperature.inCelsius)
|
||||
return ((c * magnus) / (b - magnus)).celsius
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute apparent temperature from temperature, relative humidity, and wind speed
|
||||
* Uses Bureau of Meteorology Australia methodology
|
||||
* Source: http://www.bom.gov.au/info/thermal_stress/#atapproximation
|
||||
* TODO: Unit test
|
||||
*
|
||||
* @param temperature
|
||||
* @param relativeHumidity
|
||||
* @param windSpeed
|
||||
*/
|
||||
internal fun computeApparentTemperature(
|
||||
temperature: Temperature?,
|
||||
relativeHumidity: Ratio?,
|
||||
windSpeed: Speed?,
|
||||
): Temperature? {
|
||||
if (temperature == null || relativeHumidity == null || windSpeed == null) return null
|
||||
|
||||
val e = relativeHumidity.inFraction * 6.105 * exp(17.27 * temperature.inCelsius / (237.7 + temperature.inCelsius))
|
||||
return (temperature.inCelsius + 0.33 * e - 0.7 * windSpeed.inMetersPerSecond - 4.0).celsius
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute wind chill from temperature and wind speed
|
||||
* Uses Environment Canada methodology
|
||||
* Source: https://climate.weather.gc.ca/glossary_e.html#w
|
||||
* Only valid for (T ≤ 0 °C) or (T ≤ 10°C and WS ≥ 5 km/h)
|
||||
* TODO: Unit test
|
||||
*
|
||||
* @param temperature
|
||||
* @param windSpeed
|
||||
*/
|
||||
internal fun computeWindChillTemperature(
|
||||
temperature: Temperature?,
|
||||
windSpeed: Speed?,
|
||||
): Temperature? {
|
||||
if (temperature == null || windSpeed == null || temperature > 10.celsius) return null
|
||||
return if (windSpeed >= 5.kilometersPerHour) {
|
||||
(
|
||||
13.12 +
|
||||
(0.6215 * temperature.inCelsius) -
|
||||
(11.37 * windSpeed.inKilometersPerHour.pow(0.16)) +
|
||||
(0.3965 * temperature.inCelsius * windSpeed.inKilometersPerHour.pow(0.16))
|
||||
).celsius
|
||||
} else if (temperature <= 0.celsius) {
|
||||
(temperature.inCelsius + ((-1.59 + 0.1345 * temperature.inCelsius) / 5.0) * windSpeed.inKilometersPerHour)
|
||||
.celsius
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute humidex from temperature and humidity
|
||||
* Based on formula from ECCC
|
||||
*
|
||||
* @param temperature
|
||||
* @param dewPoint
|
||||
*/
|
||||
internal fun computeHumidex(
|
||||
temperature: Temperature?,
|
||||
dewPoint: Temperature?,
|
||||
): Temperature? {
|
||||
if (temperature == null || dewPoint == null || temperature < 15.celsius) return null
|
||||
|
||||
return (
|
||||
temperature.inCelsius +
|
||||
0.5555.times(
|
||||
6.11.times(
|
||||
exp(5417.7530.times(1.div(273.15) - 1.div(273.15 + dewPoint.inCelsius)))
|
||||
).minus(10)
|
||||
)
|
||||
).celsius
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert cardinal points direction to a degree
|
||||
* Supports up to 3 characters cardinal points
|
||||
|
@ -421,108 +303,6 @@ internal fun getWindDegree(
|
|||
else -> null
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute mean sea level pressure (MSLP) from barometric pressure and altitude.
|
||||
* Optional elements can be provided for minor adjustments.
|
||||
* Source: https://integritext.net/DrKFS/correctiontosealevel.htm
|
||||
*
|
||||
* To compute barometric pressure from MSLP,
|
||||
* simply enter negative altitude.
|
||||
*
|
||||
* @param barometricPressure in hPa
|
||||
* @param altitude in meters
|
||||
* @param temperature in °C (optional)
|
||||
* @param humidity in % (optional)
|
||||
* @param latitude in ° (optional)
|
||||
*/
|
||||
internal fun computeMeanSeaLevelPressure(
|
||||
barometricPressure: Double?,
|
||||
altitude: Double?,
|
||||
temperature: Double? = null,
|
||||
humidity: Double? = null,
|
||||
latitude: Double? = null,
|
||||
): Double? {
|
||||
// There is nothing to calculate if barometric pressure or altitude is null.
|
||||
if (barometricPressure == null || altitude == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Source: http://www.bom.gov.au/info/thermal_stress/#atapproximation
|
||||
val waterVaporPressure = if (humidity != null && temperature != null) {
|
||||
humidity / 100 * 6.105 * exp(17.27 * temperature / (237.7 + temperature))
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
|
||||
// adjustment for temperature
|
||||
val term1 = 1.0 + 0.0037 * (temperature ?: 0.0)
|
||||
|
||||
// adjustment for humidity
|
||||
val term2 = 1.0 / (1.0 - 0.378 * waterVaporPressure / barometricPressure)
|
||||
|
||||
// adjustment for asphericity of the Earth
|
||||
val term3 = 1.0 / (1.0 - 0.0026 * cos(2 * (latitude ?: 45.0) * Math.PI / 180))
|
||||
|
||||
// adjustment for variation of gravitational acceleration with height
|
||||
val term4 = 1.0 + (altitude / 6367324)
|
||||
|
||||
return (10.0).pow(log10(barometricPressure) + altitude / (18400.0 * term1 * term2 * term3 * term4))
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute pollutant concentration in µg/m³ when given in ppb.
|
||||
* Can also be used for converting to mg/m³ from ppm.
|
||||
* Source: https://en.wikipedia.org/wiki/Useful_conversions_and_formulas_for_air_dispersion_modeling
|
||||
*
|
||||
* Basis for temperature and pressure assumptions:
|
||||
* https://www.ecfr.gov/current/title-40/chapter-I/subchapter-C/part-50/section-50.3
|
||||
*
|
||||
* @param pollutant one of NO2, O3, SO2 or CO
|
||||
* @param concentrationInPpb in ppb
|
||||
* @param temperature assumed 25 °C if omitted
|
||||
* @param barometricPressure assumed 1 atm = 1013.25 hPa if omitted
|
||||
*/
|
||||
internal fun computePollutantInUgm3FromPpb(
|
||||
pollutant: PollutantIndex,
|
||||
concentrationInPpb: Double?,
|
||||
temperature: Temperature? = null,
|
||||
barometricPressure: Pressure? = null,
|
||||
): Double? {
|
||||
if (concentrationInPpb == null) return null
|
||||
if (pollutant.molecularMass == null) return null
|
||||
return concentrationInPpb *
|
||||
pollutant.molecularMass /
|
||||
(8.31446261815324 / (barometricPressure?.inHectopascals ?: 1013.25) * 10) /
|
||||
(273.15 + (temperature?.inCelsius ?: 25.0))
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute pollutant concentration in ppb from µg/m³
|
||||
* Can also be used for converting to ppm from mg/m³
|
||||
* Source: https://en.wikipedia.org/wiki/Useful_conversions_and_formulas_for_air_dispersion_modeling
|
||||
*
|
||||
* Basis for temperature and pressure assumptions:
|
||||
* https://www.ecfr.gov/current/title-40/chapter-I/subchapter-C/part-50/section-50.3
|
||||
*
|
||||
* @param pollutant one of NO2, O3, SO2 or CO
|
||||
* @param concentrationInUgm3 in µg/m³
|
||||
* @param temperature assumed 25 °C if omitted
|
||||
* @param barometricPressure assumed 1 atm = 1013.25 hPa if omitted
|
||||
*/
|
||||
internal fun computePollutantInPpbFromUgm3(
|
||||
pollutant: PollutantIndex,
|
||||
concentrationInUgm3: Double?,
|
||||
temperature: Temperature? = null,
|
||||
barometricPressure: Pressure? = null,
|
||||
): Double? {
|
||||
if (concentrationInUgm3 == null) return null
|
||||
if (pollutant.molecularMass == null) return null
|
||||
return concentrationInUgm3 /
|
||||
pollutant.molecularMass *
|
||||
(8.31446261815324 / (barometricPressure?.inHectopascals ?: 1013.25) * 10) *
|
||||
(273.15 + (temperature?.inCelsius ?: 25.0))
|
||||
}
|
||||
|
||||
/**
|
||||
* DAILY FROM HOURLY
|
||||
*/
|
||||
|
|
|
@ -63,8 +63,6 @@ import org.breezyweather.common.source.WeatherSource.Companion.PRIORITY_HIGHEST
|
|||
import org.breezyweather.common.source.WeatherSource.Companion.PRIORITY_NONE
|
||||
import org.breezyweather.domain.settings.SourceConfigStore
|
||||
import org.breezyweather.domain.weather.index.PollutantIndex
|
||||
import org.breezyweather.sources.computeMeanSeaLevelPressure
|
||||
import org.breezyweather.sources.computePollutantInUgm3FromPpb
|
||||
import org.breezyweather.sources.cwa.json.CwaAirQualityResult
|
||||
import org.breezyweather.sources.cwa.json.CwaAlertResult
|
||||
import org.breezyweather.sources.cwa.json.CwaAssistantResult
|
||||
|
@ -77,6 +75,8 @@ import org.breezyweather.sources.nlsc.NlscService.Companion.MATSU_BBOX
|
|||
import org.breezyweather.sources.nlsc.NlscService.Companion.PENGHU_BBOX
|
||||
import org.breezyweather.sources.nlsc.NlscService.Companion.TAIWAN_BBOX
|
||||
import org.breezyweather.sources.nlsc.NlscService.Companion.WUQIU_BBOX
|
||||
import org.breezyweather.unit.computing.computeMeanSeaLevelPressure
|
||||
import org.breezyweather.unit.computing.computePollutantInUgm3FromPpb
|
||||
import org.breezyweather.unit.pollutant.PollutantConcentration.Companion.microgramsPerCubicMeter
|
||||
import org.breezyweather.unit.pressure.Pressure
|
||||
import org.breezyweather.unit.pressure.Pressure.Companion.hectopascals
|
||||
|
@ -506,25 +506,25 @@ class CwaService @Inject constructor(
|
|||
pM25 = it.pm25?.toDoubleOrNull()?.microgramsPerCubicMeter,
|
||||
pM10 = it.pm10?.toDoubleOrNull()?.microgramsPerCubicMeter,
|
||||
sO2 = computePollutantInUgm3FromPpb(
|
||||
PollutantIndex.SO2,
|
||||
PollutantIndex.SO2.molecularMass,
|
||||
it.so2?.toDoubleOrNull(),
|
||||
temperature,
|
||||
pressure
|
||||
)?.microgramsPerCubicMeter,
|
||||
nO2 = computePollutantInUgm3FromPpb(
|
||||
PollutantIndex.NO2,
|
||||
PollutantIndex.NO2.molecularMass,
|
||||
it.no2?.toDoubleOrNull(),
|
||||
temperature,
|
||||
pressure
|
||||
)?.microgramsPerCubicMeter,
|
||||
o3 = computePollutantInUgm3FromPpb(
|
||||
PollutantIndex.O3,
|
||||
PollutantIndex.O3.molecularMass,
|
||||
it.o3?.toDoubleOrNull(),
|
||||
temperature,
|
||||
pressure
|
||||
)?.microgramsPerCubicMeter,
|
||||
cO = computePollutantInUgm3FromPpb(
|
||||
PollutantIndex.CO,
|
||||
PollutantIndex.CO.molecularMass,
|
||||
it.co?.toDoubleOrNull(),
|
||||
temperature,
|
||||
pressure
|
||||
|
|
|
@ -51,7 +51,6 @@ import org.breezyweather.common.source.WeatherSource
|
|||
import org.breezyweather.common.source.WeatherSource.Companion.PRIORITY_HIGHEST
|
||||
import org.breezyweather.common.source.WeatherSource.Companion.PRIORITY_NONE
|
||||
import org.breezyweather.common.utils.ISO8601Utils
|
||||
import org.breezyweather.sources.computeMeanSeaLevelPressure
|
||||
import org.breezyweather.sources.getWindDegree
|
||||
import org.breezyweather.sources.nws.json.NwsAlert
|
||||
import org.breezyweather.sources.nws.json.NwsAlertsResult
|
||||
|
@ -64,6 +63,7 @@ import org.breezyweather.sources.nws.json.NwsValueDoubleContainer
|
|||
import org.breezyweather.sources.nws.json.NwsValueIntContainer
|
||||
import org.breezyweather.sources.nws.json.NwsValueWeatherContainer
|
||||
import org.breezyweather.sources.nws.json.NwsValueWeatherValue
|
||||
import org.breezyweather.unit.computing.computeMeanSeaLevelPressure
|
||||
import org.breezyweather.unit.distance.Distance.Companion.meters
|
||||
import org.breezyweather.unit.precipitation.Precipitation.Companion.millimeters
|
||||
import org.breezyweather.unit.pressure.Pressure.Companion.hectopascals
|
||||
|
|
|
@ -1,69 +0,0 @@
|
|||
/**
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.option.unit
|
||||
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.breezyweather.unit.formatting.format
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* TODO: Move to unit lib
|
||||
* TODO: Add sign testing
|
||||
*/
|
||||
class UnitUtilsTest {
|
||||
@Test
|
||||
fun formatDouble() = runTest {
|
||||
7.00646.format(
|
||||
decimals = 2,
|
||||
locale = Locale.Builder().setLanguage("fr").setRegion("FR").build()
|
||||
) shouldBe "7,01"
|
||||
7.00246.format(
|
||||
decimals = 2,
|
||||
locale = Locale.Builder().setLanguage("fr").setRegion("FR").build()
|
||||
) shouldBe "7"
|
||||
14.34234.format(
|
||||
decimals = 2,
|
||||
locale = Locale.Builder().setLanguage("fr").setRegion("FR").build()
|
||||
) shouldBe "14,34"
|
||||
14.34834.format(
|
||||
decimals = 2,
|
||||
locale = Locale.Builder().setLanguage("fr").setRegion("FR").build()
|
||||
) shouldBe "14,35"
|
||||
14.34834.format(
|
||||
decimals = 3,
|
||||
locale = Locale.Builder().setLanguage("fr").setRegion("FR").build()
|
||||
) shouldBe "14,348"
|
||||
14.34864.format(
|
||||
decimals = 3,
|
||||
locale = Locale.Builder().setLanguage("fr").setRegion("FR").build()
|
||||
) shouldBe "14,349"
|
||||
}
|
||||
|
||||
@Test
|
||||
fun formatInt() = runTest {
|
||||
14.format(
|
||||
decimals = 0,
|
||||
locale = Locale.Builder().setLanguage("fr").setRegion("FR").build()
|
||||
) shouldBe "14"
|
||||
16.format(
|
||||
decimals = 0,
|
||||
locale = Locale.Builder().setLanguage("fr").setRegion("FR").build()
|
||||
) shouldBe "16"
|
||||
}
|
||||
}
|
|
@ -2,11 +2,10 @@ package org.breezyweather.sources
|
|||
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.breezyweather.unit.temperature.Temperature.Companion.celsius
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
/**
|
||||
* To be completed
|
||||
* TODO: To be completed
|
||||
*/
|
||||
class CommonConverterTest {
|
||||
|
||||
|
@ -17,13 +16,4 @@ class CommonConverterTest {
|
|||
getWindDegree("SSO") shouldBe 202.5
|
||||
getWindDegree("VR") shouldBe -1.0
|
||||
}
|
||||
|
||||
@Test
|
||||
fun computeHumidexTest() = runTest {
|
||||
computeHumidex(null, null) shouldBe null
|
||||
computeHumidex(null, 13.0.celsius) shouldBe null
|
||||
computeHumidex(20.0.celsius, null) shouldBe null
|
||||
computeHumidex(20.0.celsius, 13.0.celsius)?.inCelsius shouldBe 22.8
|
||||
computeHumidex(39.0.celsius, 26.0.celsius)?.inCelsius shouldBe 52.5
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ You can omit any of the following properties to let the user configure their own
|
|||
*Instructions for members of the organization.*
|
||||
|
||||
1) Test your debug build.
|
||||
2) Run tests `./gradlew testBasicReleaseUnitTest`.
|
||||
2) Run tests `./gradlew testBasicDebugUnitTest` and `./gradlew testDebugUnitTest`.
|
||||
3) Try to assemble a release `./gradlew assembleBasicRelease`.
|
||||
4) Update versionCode and versionName in `app/build.gradle`.
|
||||
5) Write changelog in `CHANGELOG.md`.
|
||||
|
|
|
@ -7,6 +7,12 @@ android {
|
|||
namespace = "com.google.maps.android"
|
||||
|
||||
defaultConfig {
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
consumerProguardFiles("consumer-rules.pro")
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation(libs.bundles.test)
|
||||
testRuntimeOnly(libs.junit.platform)
|
||||
}
|
||||
|
|
|
@ -1,3 +1,19 @@
|
|||
/*
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.google.maps.android
|
||||
|
||||
import com.google.maps.android.model.LatLng
|
|
@ -12,6 +12,7 @@ Some precision may be lost during conversions.
|
|||
Remains to do:
|
||||
|
||||
- Add missing non-English Android translations (for the units we use in Breezy Weather)
|
||||
- Unit testing
|
||||
- Plus and minus operations
|
||||
- Parse from string
|
||||
|
||||
|
|
|
@ -15,4 +15,7 @@ android {
|
|||
dependencies {
|
||||
implementation(libs.annotation.jvm)
|
||||
implementation(libs.core.ktx)
|
||||
|
||||
testImplementation(libs.bundles.test)
|
||||
testRuntimeOnly(libs.junit.platform)
|
||||
}
|
||||
|
|
|
@ -213,7 +213,7 @@ interface WeatherUnit {
|
|||
locale = locale,
|
||||
showSign = showSign,
|
||||
useNumberFormatter = useNumberFormatter,
|
||||
useMeasureFormat = useMeasureFormat
|
||||
useNumberFormat = useMeasureFormat
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
@ -46,7 +46,7 @@ interface WeatherValue<T : WeatherUnit> {
|
|||
decimals = unit.getPrecision(width),
|
||||
locale = locale,
|
||||
useNumberFormatter = useNumberFormatter,
|
||||
useMeasureFormat = useMeasureFormat
|
||||
useNumberFormat = useMeasureFormat
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.unit.computing
|
||||
|
||||
import org.breezyweather.unit.ratio.Ratio
|
||||
import org.breezyweather.unit.ratio.Ratio.Companion.fraction
|
||||
import org.breezyweather.unit.temperature.Temperature
|
||||
import org.breezyweather.unit.temperature.Temperature.Companion.celsius
|
||||
import kotlin.math.exp
|
||||
import kotlin.math.ln
|
||||
|
||||
/**
|
||||
* Compute relative humidity from temperature and dew point
|
||||
* Uses Magnus approximation with Arden Buck best variable set
|
||||
* TODO: Unit test
|
||||
*/
|
||||
fun computeRelativeHumidity(
|
||||
temperature: Temperature?,
|
||||
dewPoint: Temperature?,
|
||||
): Ratio? {
|
||||
if (temperature == null || dewPoint == null) return null
|
||||
|
||||
val b = if (temperature < 0.celsius) 17.966 else 17.368
|
||||
val c = if (temperature < 0.celsius) 227.15 else 238.88 // °C
|
||||
|
||||
return (
|
||||
exp((b * dewPoint.inCelsius).div(c + dewPoint.inCelsius)) /
|
||||
exp((b * temperature.inCelsius).div(c + temperature.inCelsius))
|
||||
).fraction
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute dew point from temperature and relative humidity
|
||||
* Uses Magnus approximation with Arden Buck best variable set
|
||||
* TODO: Unit test
|
||||
*
|
||||
* @param temperature
|
||||
* @param relativeHumidity
|
||||
*/
|
||||
fun computeDewPoint(
|
||||
temperature: Temperature?,
|
||||
relativeHumidity: Ratio?,
|
||||
): Temperature? {
|
||||
if (temperature == null || relativeHumidity == null) return null
|
||||
|
||||
val b = if (temperature < 0.celsius) 17.966 else 17.368
|
||||
val c = if (temperature < 0.celsius) 227.15 else 238.88 // °C
|
||||
|
||||
val magnus = ln(relativeHumidity.inFraction) + (b * temperature.inCelsius) / (c + temperature.inCelsius)
|
||||
return ((c * magnus) / (b - magnus)).celsius
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.unit.computing
|
||||
|
||||
import org.breezyweather.unit.pressure.Pressure
|
||||
import org.breezyweather.unit.temperature.Temperature
|
||||
|
||||
/**
|
||||
* TODO: Use our typed values
|
||||
* Compute pollutant concentration in µg/m³ when given in ppb.
|
||||
* Can also be used for converting to mg/m³ from ppm.
|
||||
* Source: https://en.wikipedia.org/wiki/Useful_conversions_and_formulas_for_air_dispersion_modeling
|
||||
*
|
||||
* Basis for temperature and pressure assumptions:
|
||||
* https://www.ecfr.gov/current/title-40/chapter-I/subchapter-C/part-50/section-50.3
|
||||
*
|
||||
* @param molecularMass
|
||||
* @param concentrationInPpb in ppb
|
||||
* @param temperature assumed 25 °C if omitted
|
||||
* @param barometricPressure assumed 1 atm = 1013.25 hPa if omitted
|
||||
*/
|
||||
fun computePollutantInUgm3FromPpb(
|
||||
molecularMass: Double?,
|
||||
concentrationInPpb: Double?,
|
||||
temperature: Temperature? = null,
|
||||
barometricPressure: Pressure? = null,
|
||||
): Double? {
|
||||
if (concentrationInPpb == null) return null
|
||||
if (molecularMass == null) return null
|
||||
return concentrationInPpb *
|
||||
molecularMass /
|
||||
(8.31446261815324 / (barometricPressure?.inHectopascals ?: 1013.25) * 10) /
|
||||
(273.15 + (temperature?.inCelsius ?: 25.0))
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: Use our typed values
|
||||
* Compute pollutant concentration in ppb from µg/m³
|
||||
* Can also be used for converting to ppm from mg/m³
|
||||
* Source: https://en.wikipedia.org/wiki/Useful_conversions_and_formulas_for_air_dispersion_modeling
|
||||
*
|
||||
* Basis for temperature and pressure assumptions:
|
||||
* https://www.ecfr.gov/current/title-40/chapter-I/subchapter-C/part-50/section-50.3
|
||||
*
|
||||
* @param molecularMass
|
||||
* @param concentrationInUgm3 in µg/m³
|
||||
* @param temperature assumed 25 °C if omitted
|
||||
* @param barometricPressure assumed 1 atm = 1013.25 hPa if omitted
|
||||
*/
|
||||
fun computePollutantInPpbFromUgm3(
|
||||
molecularMass: Double?,
|
||||
concentrationInUgm3: Double?,
|
||||
temperature: Temperature? = null,
|
||||
barometricPressure: Pressure? = null,
|
||||
): Double? {
|
||||
if (concentrationInUgm3 == null) return null
|
||||
if (molecularMass == null) return null
|
||||
return concentrationInUgm3 /
|
||||
molecularMass *
|
||||
(8.31446261815324 / (barometricPressure?.inHectopascals ?: 1013.25) * 10) *
|
||||
(273.15 + (temperature?.inCelsius ?: 25.0))
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.unit.computing
|
||||
|
||||
import org.breezyweather.unit.pressure.Pressure
|
||||
import org.breezyweather.unit.ratio.Ratio
|
||||
import org.breezyweather.unit.ratio.Ratio.Companion.fraction
|
||||
import org.breezyweather.unit.speed.Speed
|
||||
import org.breezyweather.unit.speed.Speed.Companion.kilometersPerHour
|
||||
import org.breezyweather.unit.temperature.Temperature
|
||||
import org.breezyweather.unit.temperature.Temperature.Companion.celsius
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.exp
|
||||
import kotlin.math.ln
|
||||
import kotlin.math.log10
|
||||
import kotlin.math.pow
|
||||
|
||||
/**
|
||||
* TODO: Use our typed values
|
||||
* Compute mean sea level pressure (MSLP) from barometric pressure and altitude.
|
||||
* Optional elements can be provided for minor adjustments.
|
||||
* Source: https://integritext.net/DrKFS/correctiontosealevel.htm
|
||||
*
|
||||
* To compute barometric pressure from MSLP,
|
||||
* simply enter negative altitude.
|
||||
*
|
||||
* @param barometricPressure in hPa
|
||||
* @param altitude in meters
|
||||
* @param temperature in °C (optional)
|
||||
* @param humidity in % (optional)
|
||||
* @param latitude in ° (optional)
|
||||
*/
|
||||
fun computeMeanSeaLevelPressure(
|
||||
barometricPressure: Double?,
|
||||
altitude: Double?,
|
||||
temperature: Double? = null,
|
||||
humidity: Double? = null,
|
||||
latitude: Double? = null,
|
||||
): Double? {
|
||||
// There is nothing to calculate if barometric pressure or altitude is null.
|
||||
if (barometricPressure == null || altitude == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Source: http://www.bom.gov.au/info/thermal_stress/#atapproximation
|
||||
val waterVaporPressure = if (humidity != null && temperature != null) {
|
||||
humidity / 100 * 6.105 * exp(17.27 * temperature / (237.7 + temperature))
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
|
||||
// adjustment for temperature
|
||||
val term1 = 1.0 + 0.0037 * (temperature ?: 0.0)
|
||||
|
||||
// adjustment for humidity
|
||||
val term2 = 1.0 / (1.0 - 0.378 * waterVaporPressure / barometricPressure)
|
||||
|
||||
// adjustment for asphericity of the Earth
|
||||
val term3 = 1.0 / (1.0 - 0.0026 * cos(2 * (latitude ?: 45.0) * Math.PI / 180))
|
||||
|
||||
// adjustment for variation of gravitational acceleration with height
|
||||
val term4 = 1.0 + (altitude / 6367324)
|
||||
|
||||
return (10.0).pow(log10(barometricPressure) + altitude / (18400.0 * term1 * term2 * term3 * term4))
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.unit.computing
|
||||
|
||||
import org.breezyweather.unit.pressure.Pressure
|
||||
import org.breezyweather.unit.ratio.Ratio
|
||||
import org.breezyweather.unit.ratio.Ratio.Companion.fraction
|
||||
import org.breezyweather.unit.speed.Speed
|
||||
import org.breezyweather.unit.speed.Speed.Companion.kilometersPerHour
|
||||
import org.breezyweather.unit.temperature.Temperature
|
||||
import org.breezyweather.unit.temperature.Temperature.Companion.celsius
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.exp
|
||||
import kotlin.math.ln
|
||||
import kotlin.math.log10
|
||||
import kotlin.math.pow
|
||||
|
||||
/**
|
||||
* Compute apparent temperature from temperature, relative humidity, and wind speed
|
||||
* Uses Bureau of Meteorology Australia methodology
|
||||
* Source: http://www.bom.gov.au/info/thermal_stress/#atapproximation
|
||||
* TODO: Unit test
|
||||
*
|
||||
* @param temperature
|
||||
* @param relativeHumidity
|
||||
* @param windSpeed
|
||||
*/
|
||||
fun computeApparentTemperature(
|
||||
temperature: Temperature?,
|
||||
relativeHumidity: Ratio?,
|
||||
windSpeed: Speed?,
|
||||
): Temperature? {
|
||||
if (temperature == null || relativeHumidity == null || windSpeed == null) return null
|
||||
|
||||
val e = relativeHumidity.inFraction * 6.105 * exp(17.27 * temperature.inCelsius / (237.7 + temperature.inCelsius))
|
||||
return (temperature.inCelsius + 0.33 * e - 0.7 * windSpeed.inMetersPerSecond - 4.0).celsius
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute wind chill from temperature and wind speed
|
||||
* Uses Environment Canada methodology
|
||||
* Source: https://climate.weather.gc.ca/glossary_e.html#w
|
||||
* Only valid for (T ≤ 0 °C) or (T ≤ 10°C and WS ≥ 5 km/h)
|
||||
* TODO: Unit test
|
||||
*
|
||||
* @param temperature
|
||||
* @param windSpeed
|
||||
*/
|
||||
fun computeWindChillTemperature(
|
||||
temperature: Temperature?,
|
||||
windSpeed: Speed?,
|
||||
): Temperature? {
|
||||
if (temperature == null || windSpeed == null || temperature > 10.celsius) return null
|
||||
return if (windSpeed >= 5.kilometersPerHour) {
|
||||
(
|
||||
13.12 +
|
||||
(0.6215 * temperature.inCelsius) -
|
||||
(11.37 * windSpeed.inKilometersPerHour.pow(0.16)) +
|
||||
(0.3965 * temperature.inCelsius * windSpeed.inKilometersPerHour.pow(0.16))
|
||||
).celsius
|
||||
} else if (temperature <= 0.celsius) {
|
||||
(temperature.inCelsius + ((-1.59 + 0.1345 * temperature.inCelsius) / 5.0) * windSpeed.inKilometersPerHour)
|
||||
.celsius
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute humidex from temperature and humidity
|
||||
* Based on formula from ECCC
|
||||
*
|
||||
* @param temperature
|
||||
* @param dewPoint
|
||||
*/
|
||||
fun computeHumidex(
|
||||
temperature: Temperature?,
|
||||
dewPoint: Temperature?,
|
||||
): Temperature? {
|
||||
if (temperature == null || dewPoint == null || temperature < 15.celsius) return null
|
||||
|
||||
return (
|
||||
temperature.inCelsius +
|
||||
0.5555.times(
|
||||
6.11.times(
|
||||
exp(5417.7530.times(1.div(273.15) - 1.div(273.15 + dewPoint.inCelsius)))
|
||||
).minus(10)
|
||||
)
|
||||
).celsius
|
||||
}
|
|
@ -59,7 +59,7 @@ fun Duration.formatValue(
|
|||
decimals = unit.getPrecision(width),
|
||||
locale = locale,
|
||||
useNumberFormatter = useNumberFormatter,
|
||||
useMeasureFormat = useMeasureFormat
|
||||
useNumberFormat = useMeasureFormat
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -35,7 +35,7 @@ fun Number.format(
|
|||
locale: Locale = Locale.getDefault(),
|
||||
showSign: Boolean = false,
|
||||
useNumberFormatter: Boolean = true,
|
||||
useMeasureFormat: Boolean = true,
|
||||
useNumberFormat: Boolean = true,
|
||||
): String {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && useNumberFormatter) {
|
||||
(NumberFormatter.withLocale(locale) as LocalizedNumberFormatter)
|
||||
|
@ -43,7 +43,7 @@ fun Number.format(
|
|||
.sign(if (showSign) NumberFormatter.SignDisplay.ALWAYS else NumberFormatter.SignDisplay.AUTO)
|
||||
.format(this)
|
||||
.toString()
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && useMeasureFormat && !showSign) {
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && useNumberFormat && !showSign) {
|
||||
// showSign not supported by NumberFormat, skip
|
||||
NumberFormat.getNumberInstance(locale)
|
||||
.apply { maximumFractionDigits = decimals }
|
||||
|
|
|
@ -93,7 +93,7 @@ enum class RatioUnit(
|
|||
locale = locale,
|
||||
showSign = showSign,
|
||||
useNumberFormatter = useNumberFormatter,
|
||||
useMeasureFormat = useMeasureFormat
|
||||
useNumberFormat = useMeasureFormat
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.unit.computing
|
||||
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.breezyweather.unit.temperature.Temperature.Companion.celsius
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
/**
|
||||
* TODO: To be completed
|
||||
*/
|
||||
class TemperatureComputingTest {
|
||||
|
||||
@Test
|
||||
fun computeHumidexTest() = runTest {
|
||||
computeHumidex(null, null) shouldBe null
|
||||
computeHumidex(null, 13.0.celsius) shouldBe null
|
||||
computeHumidex(20.0.celsius, null) shouldBe null
|
||||
computeHumidex(20.0.celsius, 13.0.celsius)?.inCelsius shouldBe 22.8
|
||||
computeHumidex(39.0.celsius, 26.0.celsius)?.inCelsius shouldBe 52.5
|
||||
}
|
||||
}
|
|
@ -0,0 +1,159 @@
|
|||
/*
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.unit.formatting
|
||||
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* TODO: Currently, NumberFormatter and NumberFormat never works, as SDK is always 0
|
||||
* TODO: Thousand separator tests
|
||||
*/
|
||||
class NumberFormattingTest {
|
||||
@Test
|
||||
fun `Format double with decimals and French-style number formatting, using NumberFormatter`() = runTest {
|
||||
formatDoubleWithDecimalsAndFrenchLocale(useNumberFormatter = true, useNumberFormat = false)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Format double with decimals and French-style number formatting, using NumberFormat`() = runTest {
|
||||
formatDoubleWithDecimalsAndFrenchLocale(useNumberFormatter = false, useNumberFormat = true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Format double with decimals and French-style number formatting, using DecimalFormat`() = runTest {
|
||||
formatDoubleWithDecimalsAndFrenchLocale(useNumberFormatter = false, useNumberFormat = false)
|
||||
}
|
||||
|
||||
fun formatDoubleWithDecimalsAndFrenchLocale(
|
||||
useNumberFormatter: Boolean,
|
||||
useNumberFormat: Boolean,
|
||||
) {
|
||||
7.00646.format(
|
||||
decimals = 2,
|
||||
locale = Locale.Builder().setLanguage("fr").setRegion("FR").build(),
|
||||
showSign = false,
|
||||
useNumberFormatter = useNumberFormatter,
|
||||
useNumberFormat = useNumberFormat
|
||||
) shouldBe "7,01"
|
||||
14.34234.format(
|
||||
decimals = 2,
|
||||
locale = Locale.Builder().setLanguage("fr").setRegion("FR").build(),
|
||||
showSign = false,
|
||||
useNumberFormatter = useNumberFormatter,
|
||||
useNumberFormat = useNumberFormat
|
||||
) shouldBe "14,34"
|
||||
14.34834.format(
|
||||
decimals = 2,
|
||||
locale = Locale.Builder().setLanguage("fr").setRegion("FR").build(),
|
||||
showSign = false,
|
||||
useNumberFormatter = useNumberFormatter,
|
||||
useNumberFormat = useNumberFormat
|
||||
) shouldBe "14,35"
|
||||
14.34834.format(
|
||||
decimals = 3,
|
||||
locale = Locale.Builder().setLanguage("fr").setRegion("FR").build(),
|
||||
showSign = false,
|
||||
useNumberFormatter = useNumberFormatter,
|
||||
useNumberFormat = useNumberFormat
|
||||
) shouldBe "14,348"
|
||||
14.34864.format(
|
||||
decimals = 3,
|
||||
locale = Locale.Builder().setLanguage("fr").setRegion("FR").build(),
|
||||
showSign = false,
|
||||
useNumberFormatter = useNumberFormatter,
|
||||
useNumberFormat = useNumberFormat
|
||||
) shouldBe "14,349"
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Format double without values ending with a 0`() = runTest {
|
||||
7.00246.format(
|
||||
decimals = 2,
|
||||
locale = Locale.Builder().setLanguage("en").setRegion("US").build(),
|
||||
showSign = false,
|
||||
useNumberFormatter = true,
|
||||
useNumberFormat = false
|
||||
) shouldBe "7"
|
||||
7.00246.format(
|
||||
decimals = 2,
|
||||
locale = Locale.Builder().setLanguage("en").setRegion("US").build(),
|
||||
showSign = false,
|
||||
useNumberFormatter = false,
|
||||
useNumberFormat = true
|
||||
) shouldBe "7"
|
||||
7.00246.format(
|
||||
decimals = 2,
|
||||
locale = Locale.Builder().setLanguage("en").setRegion("US").build(),
|
||||
showSign = false,
|
||||
useNumberFormatter = false,
|
||||
useNumberFormat = false
|
||||
) shouldBe "7"
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Format int`() = runTest {
|
||||
14.format(
|
||||
decimals = 0,
|
||||
locale = Locale.Builder().setLanguage("en").setRegion("US").build(),
|
||||
showSign = false,
|
||||
useNumberFormatter = true,
|
||||
useNumberFormat = false
|
||||
) shouldBe "14"
|
||||
14.format(
|
||||
decimals = 0,
|
||||
locale = Locale.Builder().setLanguage("en").setRegion("US").build(),
|
||||
showSign = false,
|
||||
useNumberFormatter = false,
|
||||
useNumberFormat = true
|
||||
) shouldBe "14"
|
||||
14.format(
|
||||
decimals = 0,
|
||||
locale = Locale.Builder().setLanguage("en").setRegion("US").build(),
|
||||
showSign = false,
|
||||
useNumberFormatter = false,
|
||||
useNumberFormat = false
|
||||
) shouldBe "14"
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Format with leading sign`() = runTest {
|
||||
7.3.format(
|
||||
decimals = 1,
|
||||
locale = Locale.Builder().setLanguage("en").setRegion("US").build(),
|
||||
showSign = true,
|
||||
useNumberFormatter = true,
|
||||
useNumberFormat = false
|
||||
) shouldBe "+7.3"
|
||||
7.3.format(
|
||||
decimals = 1,
|
||||
locale = Locale.Builder().setLanguage("en").setRegion("US").build(),
|
||||
showSign = true,
|
||||
useNumberFormatter = false,
|
||||
useNumberFormat = true
|
||||
) shouldBe "+7.3"
|
||||
7.3.format(
|
||||
decimals = 1,
|
||||
locale = Locale.Builder().setLanguage("en").setRegion("US").build(),
|
||||
showSign = true,
|
||||
useNumberFormatter = false,
|
||||
useNumberFormat = false
|
||||
) shouldBe "+7.3"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
* This file is part of Breezy Weather.
|
||||
*
|
||||
* Breezy Weather is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Lesser General Public License as published by the
|
||||
* Free Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Breezy Weather is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Breezy Weather. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.breezyweather.unit.temperature
|
||||
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.breezyweather.unit.temperature.Temperature.Companion.celsius
|
||||
import org.breezyweather.unit.temperature.Temperature.Companion.deciCelsius
|
||||
import org.breezyweather.unit.temperature.Temperature.Companion.fahrenheit
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
/**
|
||||
* TODO:
|
||||
* - Deviation conversion
|
||||
* - Formatting with Android 16 (currently, SDK is always 0)
|
||||
* - Display name
|
||||
*/
|
||||
class TemperatureTest {
|
||||
|
||||
@Test
|
||||
fun `convert from reference unit to Fahrenheit`() = runTest {
|
||||
0.deciCelsius.inFahrenheit shouldBe 32.0
|
||||
100.deciCelsius.inFahrenheit shouldBe 50.0
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `convert from Fahrenheit to reference unit`() = runTest {
|
||||
32.0.fahrenheit.value shouldBe 0L
|
||||
50.0.fahrenheit.value shouldBe 100L
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `convert from Celsius to Fahrenheit`() = runTest {
|
||||
0.celsius.inFahrenheit shouldBe 32.0
|
||||
10.celsius.inFahrenheit shouldBe 50.0
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `convert from Celsius to Kelvin`() = runTest {
|
||||
0.celsius.inKelvins shouldBe 273.15
|
||||
10.celsius.inKelvins shouldBe 283.15
|
||||
}
|
||||
|
||||
/*@Test
|
||||
fun `format Celsius with narrow unit width`() = runTest {
|
||||
val context = mockk<Context>().apply {
|
||||
every { getString(any()) } returns "FAILED"
|
||||
every { getString(org.breezyweather.unit.R.string.temperature_c_nominative_narrow, any()) } returns "%s°"
|
||||
every { getString(org.breezyweather.unit.R.string.temperature_k_nominative_narrow, any()) } returns "%sK"
|
||||
}
|
||||
101.4.deciCelsius.format(
|
||||
context = context,
|
||||
unit = TemperatureUnit.CELSIUS,
|
||||
valueWidth = UnitWidth.NARROW,
|
||||
unitWidth = UnitWidth.NARROW,
|
||||
locale = Locale.Builder().setLanguage("fr").setRegion("FR").build()
|
||||
) shouldBe "10°"
|
||||
101.4.deciCelsius.format(
|
||||
context = context,
|
||||
unit = TemperatureUnit.CELSIUS,
|
||||
valueWidth = UnitWidth.SHORT,
|
||||
unitWidth = UnitWidth.NARROW,
|
||||
locale = Locale.Builder().setLanguage("fr").setRegion("FR").build()
|
||||
) shouldBe "10,1°"
|
||||
0.deciCelsius.format(
|
||||
context = context,
|
||||
unit = TemperatureUnit.KELVIN,
|
||||
valueWidth = UnitWidth.LONG,
|
||||
unitWidth = UnitWidth.NARROW,
|
||||
locale = Locale.Builder().setLanguage("fr").setRegion("FR").build()
|
||||
) shouldBe "273,15K"
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `format Celsius with short unit width`() = runTest {
|
||||
val context = mockk<Context>().apply {
|
||||
every { getString(any()) } returns "FAILED"
|
||||
every { getString(org.breezyweather.unit.R.string.temperature_c_nominative_short, any()) } returns "%s °C"
|
||||
every { getString(org.breezyweather.unit.R.string.temperature_k_nominative_short, any()) } returns "%s K"
|
||||
}
|
||||
101.4.deciCelsius.format(
|
||||
context = context,
|
||||
unit = TemperatureUnit.CELSIUS,
|
||||
valueWidth = UnitWidth.NARROW,
|
||||
unitWidth = UnitWidth.SHORT,
|
||||
locale = Locale.Builder().setLanguage("fr").setRegion("FR").build()
|
||||
) shouldBe "10 °C"
|
||||
101.4.deciCelsius.format(
|
||||
context = context,
|
||||
unit = TemperatureUnit.CELSIUS,
|
||||
valueWidth = UnitWidth.SHORT,
|
||||
unitWidth = UnitWidth.SHORT,
|
||||
locale = Locale.Builder().setLanguage("fr").setRegion("FR").build()
|
||||
) shouldBe "10,1 °C"
|
||||
0.deciCelsius.format(
|
||||
context = context,
|
||||
unit = TemperatureUnit.KELVIN,
|
||||
valueWidth = UnitWidth.LONG,
|
||||
unitWidth = UnitWidth.SHORT,
|
||||
locale = Locale.Builder().setLanguage("fr").setRegion("FR").build()
|
||||
) shouldBe "273,15 K"
|
||||
}*/
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue