Major classes refactoring and code cleanup

This commit is contained in:
Arty Bishop committed 2025-12-27 15:01:33 +00:00
1 parent 6572dd4eab
commit f7f317cf8b
67 files changed
+610 -754

No files matched your search

@@ -15,50 +15,52 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.utility
package com.rtbishop.look4sat.data
import com.rtbishop.look4sat.domain.model.SatRadio
import com.rtbishop.look4sat.domain.predict.OrbitalData
import java.io.InputStream
import kotlin.math.pow
import com.rtbishop.look4sat.model.OrbitalData
import com.rtbishop.look4sat.model.SatRadio
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import org.json.JSONArray
import org.json.JSONObject
import java.io.InputStream
import kotlin.math.pow
class DataParser(private val defaultDispatcher: CoroutineDispatcher) {
suspend fun parseCSVStream(csvStream: InputStream): List<OrbitalData> = withContext(defaultDispatcher) {
val parsedItems = mutableListOf<OrbitalData>()
csvStream.bufferedReader().useLines { lines ->
lines.forEachIndexed { index, line ->
if (index != 0) {
val values = line.split(",")
parseCSV(values)?.let { tle -> parsedItems.add(tle) }
suspend fun parseCSVStream(csvStream: InputStream): List<OrbitalData> =
withContext(defaultDispatcher) {
val parsedItems = mutableListOf<OrbitalData>()
csvStream.bufferedReader().useLines { lines ->
lines.forEachIndexed { index, line ->
if (index != 0) {
val values = line.split(",")
parseCSV(values)?.let { tle -> parsedItems.add(tle) }
}
}
}
return@withContext parsedItems
}
return@withContext parsedItems
}
suspend fun parseTLEStream(tleStream: InputStream): List<OrbitalData> = withContext(defaultDispatcher) {
val tleStrings = mutableListOf(String(), String(), String())
val parsedItems = mutableListOf<OrbitalData>()
var lineIndex = 0
tleStream.bufferedReader().forEachLine { line ->
tleStrings[lineIndex] = line
if (lineIndex < 2) {
lineIndex++
} else {
val isLineOneValid = tleStrings[1].substring(0, 1) == "1"
val isLineTwoValid = tleStrings[2].substring(0, 1) == "2"
if (!isLineOneValid && !isLineTwoValid) return@forEachLine
parseTLE(tleStrings)?.let { tle -> parsedItems.add(tle) }
lineIndex = 0
suspend fun parseTLEStream(tleStream: InputStream): List<OrbitalData> =
withContext(defaultDispatcher) {
val tleStrings = mutableListOf(String(), String(), String())
val parsedItems = mutableListOf<OrbitalData>()
var lineIndex = 0
tleStream.bufferedReader().forEachLine { line ->
tleStrings[lineIndex] = line
if (lineIndex < 2) {
lineIndex++
} else {
val isLineOneValid = tleStrings[1].substring(0, 1) == "1"
val isLineTwoValid = tleStrings[2].substring(0, 1) == "2"
if (!isLineOneValid && !isLineTwoValid) return@forEachLine
parseTLE(tleStrings)?.let { tle -> parsedItems.add(tle) }
lineIndex = 0
}
}
return@withContext parsedItems
}
return@withContext parsedItems
}
suspend fun parseJSONStream(jsonStream: InputStream): List<SatRadio> {
return withContext(defaultDispatcher) {
@@ -76,10 +78,6 @@ class DataParser(private val defaultDispatcher: CoroutineDispatcher) {
}
}
fun isLeapYear(year: Int): Boolean {
return ((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0)
}
private fun parseCSV(values: List<String>): OrbitalData? {
try {
val name = values[0]
@@ -122,8 +120,8 @@ class DataParser(private val defaultDispatcher: CoroutineDispatcher) {
val argper: Double = tle[2].substring(34, 42).toDouble()
val meanan: Double = tle[2].substring(43, 51).toDouble()
val catnum: Int = tle[1].substring(2, 7).trim().toInt()
val bstar: Double =
1.0e-5 * tle[1].substring(53, 59).toDouble() / 10.0.pow(tle[1].substring(60, 61).toDouble())
val bstar: Double = 1.0e-5 * tle[1].substring(53, 59).toDouble() /
10.0.pow(tle[1].substring(60, 61).toDouble())
return OrbitalData(name, epoch, meanmo, eccn, incl, raan, argper, meanan, catnum, bstar)
} catch (exception: Exception) {
return null
@@ -155,10 +153,8 @@ class DataParser(private val defaultDispatcher: CoroutineDispatcher) {
val daysArray = arrayOf(31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
var dayOfYear = dayOfMonth
// If leap year increment Feb days
if (isLeapYear(year)) daysArray[1]++
for (i in 0 until month - 1) {
dayOfYear += daysArray[i]
}
if (((year / 4 == 0) && (year / 100 != 0)) || (year / 400 == 0)) daysArray[1]++
for (i in 0 until month - 1) { dayOfYear += daysArray[i] }
return dayOfYear
}
}
@@ -15,13 +15,12 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.domain.data
package com.rtbishop.look4sat.data
import com.rtbishop.look4sat.domain.IDataRepository
import com.rtbishop.look4sat.domain.ISettingsManager
import com.rtbishop.look4sat.domain.model.DataState
import com.rtbishop.look4sat.domain.model.SatEntry
import com.rtbishop.look4sat.utility.DataParser
import com.rtbishop.look4sat.domain.ISettingsSource
import com.rtbishop.look4sat.model.DataState
import com.rtbishop.look4sat.model.SatEntry
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -30,16 +29,16 @@ import java.util.zip.ZipInputStream
class DataRepository(
private val dataParser: DataParser,
private val fileSource: IFileDataSource,
private val entrySource: ILocalEntrySource,
private val radioSource: ILocalRadioSource,
private val remoteSource: IRemoteDataSource,
private val fileSource: IFileSource,
private val entrySource: IEntriesStorage,
private val radioSource: IRadiosStorage,
private val remoteSource: INetworkSource,
private val repositoryScope: CoroutineScope,
private val settingsManager: ISettingsManager
private val settingsManager: ISettingsSource
) : IDataRepository {
private val exceptionHandler = CoroutineExceptionHandler { _, exception ->
_updateState.value = DataState.Error(exception.message)
_updateState.value = DataState.Exception(exception.message)
}
private val updateStateDelay = 875L
private val _updateState = MutableStateFlow<DataState<Long>>(DataState.Handled)
@@ -15,14 +15,14 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.domain.data
package com.rtbishop.look4sat.data
import com.rtbishop.look4sat.domain.model.SatEntry
import com.rtbishop.look4sat.domain.model.SatItem
import com.rtbishop.look4sat.domain.predict.Satellite
import com.rtbishop.look4sat.domain.Satellite
import com.rtbishop.look4sat.model.SatEntry
import com.rtbishop.look4sat.model.SatItem
import kotlinx.coroutines.flow.Flow
interface ILocalEntrySource {
interface IEntriesStorage {
fun getEntriesTotal(): Flow<Int>
@@ -15,11 +15,11 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.domain.data
package com.rtbishop.look4sat.data
import java.io.InputStream
interface IFileDataSource {
interface IFileSource {
suspend fun getDataStream(uri: String): InputStream?
}
@@ -15,11 +15,11 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.domain.data
package com.rtbishop.look4sat.data
import java.io.InputStream
interface IRemoteDataSource {
interface INetworkSource {
val radioApi: String get() = "https://db.satnogs.org/api/transmitters/?format=json&status=active"
@@ -15,12 +15,12 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.domain.data
package com.rtbishop.look4sat.data
import com.rtbishop.look4sat.domain.model.SatRadio
import com.rtbishop.look4sat.model.SatRadio
import kotlinx.coroutines.flow.Flow
interface ILocalRadioSource {
interface IRadiosStorage {
fun getRadiosTotal(): Flow<Int>
@@ -15,8 +15,9 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.domain.predict
package com.rtbishop.look4sat.domain
import com.rtbishop.look4sat.model.OrbitalData
import kotlin.math.*
class DeepSpaceSat(data: OrbitalData) : Satellite(data) {
@@ -17,10 +17,9 @@
*/
package com.rtbishop.look4sat.domain
import com.rtbishop.look4sat.domain.model.DataState
import com.rtbishop.look4sat.domain.model.SatItem
import com.rtbishop.look4sat.domain.model.SatRadio
import com.rtbishop.look4sat.domain.predict.Satellite
import com.rtbishop.look4sat.model.DataState
import com.rtbishop.look4sat.model.SatItem
import com.rtbishop.look4sat.model.SatRadio
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
@@ -1,41 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.domain
import com.rtbishop.look4sat.domain.model.DataState
import com.rtbishop.look4sat.domain.predict.GeoPos
import kotlinx.coroutines.flow.SharedFlow
interface ILocationManager {
val stationPosition: SharedFlow<DataState<GeoPos>>
fun getStationLocator(): String
fun getStationPosition(): GeoPos
fun setStationPosition(latitude: Double, longitude: Double)
fun setPositionFromGps()
fun setPositionFromNet()
fun setPositionFromQth(locator: String)
fun setPositionHandled()
}
@@ -0,0 +1,32 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.domain
import com.rtbishop.look4sat.model.StationPos
import kotlinx.coroutines.flow.StateFlow
interface ILocationSource {
val stationPosition: StateFlow<StationPos>
fun setGpsPosition(): Boolean
fun setGeoPosition(latitude: Double, longitude: Double): Boolean
fun setQthPosition(locator: String): Boolean
}
@@ -1,10 +1,9 @@
package com.rtbishop.look4sat.domain
import com.rtbishop.look4sat.domain.model.SatRadio
import com.rtbishop.look4sat.domain.predict.GeoPos
import com.rtbishop.look4sat.domain.predict.SatPass
import com.rtbishop.look4sat.domain.predict.SatPos
import com.rtbishop.look4sat.domain.predict.Satellite
import com.rtbishop.look4sat.model.GeoPos
import com.rtbishop.look4sat.model.SatPass
import com.rtbishop.look4sat.model.SatPos
import com.rtbishop.look4sat.model.SatRadio
import kotlinx.coroutines.flow.SharedFlow
interface ISatelliteManager {
@@ -17,9 +17,9 @@
*/
package com.rtbishop.look4sat.domain
import com.rtbishop.look4sat.domain.predict.GeoPos
import com.rtbishop.look4sat.model.StationPos
interface ISettingsManager {
interface ISettingsSource {
val sourcesMap: Map<String, String>
get() = mapOf(
@@ -51,13 +51,9 @@ interface ISettingsManager {
"Amsat" to "https://amsat.org/tle/current/nasabare.txt"
)
fun loadStationLocator(): String
fun loadStationPosition(): StationPos
fun saveStationLocator(locator: String)
fun loadStationPosition(): GeoPos
fun saveStationPosition(position: GeoPos)
fun saveStationPosition(stationPos: StationPos)
fun getHoursAhead(): Int
@@ -15,8 +15,9 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.domain.predict
package com.rtbishop.look4sat.domain
import com.rtbishop.look4sat.model.OrbitalData
import kotlin.math.*
class NearEarthSat(data: OrbitalData) : Satellite(data) {
@@ -15,8 +15,11 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.domain.predict
package com.rtbishop.look4sat.domain
import com.rtbishop.look4sat.model.GeoPos
import com.rtbishop.look4sat.model.OrbitalData
import com.rtbishop.look4sat.model.SatPos
import kotlin.math.*
const val ASTRONOMICAL_UNIT = 1.49597870691E8
@@ -15,10 +15,12 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.domain.predict
package com.rtbishop.look4sat.domain
import com.rtbishop.look4sat.domain.ISatelliteManager
import com.rtbishop.look4sat.domain.model.SatRadio
import com.rtbishop.look4sat.model.GeoPos
import com.rtbishop.look4sat.model.SatPass
import com.rtbishop.look4sat.model.SatPos
import com.rtbishop.look4sat.model.SatRadio
import com.rtbishop.look4sat.utility.round
import com.rtbishop.look4sat.utility.toDegrees
import kotlinx.coroutines.CoroutineDispatcher
@@ -15,11 +15,12 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.domain.model
package com.rtbishop.look4sat.model
sealed class DataState<out T> {
data class Success<out T>(val data: T) : DataState<T>()
data class Error(val message: String?) : DataState<Nothing>()
data class Error(val resId: Int?) : DataState<Nothing>()
data class Exception(val message: String?) : DataState<Nothing>()
object Handled : DataState<Nothing>()
object Loading : DataState<Nothing>()
}
@@ -15,6 +15,6 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.domain.predict
package com.rtbishop.look4sat.model
data class GeoPos(val lat: Double, val lon: Double, val alt: Double = 0.0, val name: String? = null)
@@ -15,7 +15,9 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.domain.predict
package com.rtbishop.look4sat.model
import com.rtbishop.look4sat.domain.*
data class OrbitalData(
val name: String,
@@ -15,8 +15,6 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.domain.model
import com.rtbishop.look4sat.domain.predict.OrbitalData
package com.rtbishop.look4sat.model
data class SatEntry(val data: OrbitalData, var comment: String? = null)
@@ -15,7 +15,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.domain.model
package com.rtbishop.look4sat.model
data class SatItem(
val catnum: Int,
@@ -15,7 +15,9 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.domain.predict
package com.rtbishop.look4sat.model
import com.rtbishop.look4sat.domain.Satellite
data class SatPass(
val aosTime: Long = 0L,
@@ -15,8 +15,12 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.domain.predict
package com.rtbishop.look4sat.model
import com.rtbishop.look4sat.domain.DEG2RAD
import com.rtbishop.look4sat.domain.EARTH_RADIUS
import com.rtbishop.look4sat.domain.RAD2DEG
import com.rtbishop.look4sat.domain.SPEED_OF_LIGHT
import kotlin.math.*
data class SatPos(
@@ -15,7 +15,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.domain.model
package com.rtbishop.look4sat.model
data class SatRadio(
val uuid: String,
@@ -0,0 +1,7 @@
package com.rtbishop.look4sat.model
data class StationPos(
val latitude: Double, val longitude: Double, val qthLocator: String, val timestamp: Long
) {
fun toGeoPos(): GeoPos = GeoPos(latitude, longitude)
}
@@ -1,70 +0,0 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2022 Arty Bishop (bishop.arty@gmail.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.utility
import java.net.InetSocketAddress
import java.nio.ByteBuffer
import java.nio.channels.SocketChannel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.launch
class DataReporter(private val reporterScope: CoroutineScope) {
private var rotationSocketChannel: SocketChannel? = null
private var rotationReporting: Job? = null
// private var frequencySocketChannel: SocketChannel? = null
// private var frequencyReporting: Job? = null
//
// fun reportFrequency(server: String, port: Int, frequency: Long) {
// frequencyReporting = reporterScope.launch {
// runCatching {
// if (frequencySocketChannel == null) {
// frequencySocketChannel = SocketChannel.open(InetSocketAddress(server, port))
// } else {
// val buffer = ByteBuffer.wrap("\\set_freq $frequency\n".toByteArray())
// frequencySocketChannel?.write(buffer)
// }
// }.onFailure { error ->
// println(error.localizedMessage)
// frequencySocketChannel = null
// frequencyReporting?.cancelAndJoin()
// }
// }
// }
fun reportRotation(server: String, port: Int, azimuth: Double, elevation: Double) {
rotationReporting = reporterScope.launch {
val newElevation = if (elevation > 0.0) elevation else 0.0
runCatching {
if (rotationSocketChannel == null) {
rotationSocketChannel = SocketChannel.open(InetSocketAddress(server, port))
} else {
val buffer = ByteBuffer.wrap("\\P $azimuth $newElevation\n".toByteArray())
rotationSocketChannel?.write(buffer)
}
}.onFailure { error ->
println(error.localizedMessage)
rotationSocketChannel = null
rotationReporting?.cancelAndJoin()
}
}
}
}
@@ -17,8 +17,8 @@
*/
package com.rtbishop.look4sat.utility
import com.rtbishop.look4sat.domain.predict.DEG2RAD
import com.rtbishop.look4sat.domain.predict.RAD2DEG
import com.rtbishop.look4sat.domain.DEG2RAD
import com.rtbishop.look4sat.domain.RAD2DEG
import java.util.concurrent.TimeUnit
fun Long.toTimerString(): String {
@@ -64,7 +64,7 @@ fun Double.toRadians(): Double = this * DEG2RAD
//}
fun String.isValidIPv4(): Boolean {
val ip4 = "^(([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])(\\.(?!\$)|\$)){4}\$"
val ip4 = "^((\\d|[1-9]\\d|1\\d\\d|2[0-4]\\d|25[0-5])(\\.(?!\$)|\$)){4}\$"
return this.matches(ip4.toRegex())
}
@@ -17,7 +17,7 @@
*/
package com.rtbishop.look4sat.utility
import com.rtbishop.look4sat.domain.predict.GeoPos
import com.rtbishop.look4sat.model.GeoPos
object QthConverter {
@@ -35,27 +35,27 @@ object QthConverter {
return GeoPos(latitude, longitude)
}
fun positionToQth(lat: Double, lon: Double): String? {
if (!isValidPosition(lat, lon)) return null
val tempLon = if (lon > 180.0) lon - 180 else lon
fun positionToQth(latitude: Double, longitude: Double): String? {
if (!isValidPosition(latitude, longitude)) return null
val tempLon = if (longitude > 180.0) longitude - 180 else longitude
val upper = "ABCDEFGHIJKLMNOPQRSTUVWX"
val lower = "abcdefghijklmnopqrstuvwx"
val longitude = tempLon + 180
val latitude = lat + 90
val lonFirst = upper[(longitude / 20).toInt()]
val latFirst = upper[(latitude / 10).toInt()]
val lonSecond = ((longitude / 2) % 10).toInt().toString()
val latSecond = (latitude % 10).toInt().toString()
val lonThird = lower[((longitude % 2) * 12).toInt()]
val latThird = lower[((latitude % 1) * 24).toInt()]
val newLongitude = tempLon + 180
val newLatitude = latitude + 90
val lonFirst = upper[(newLongitude / 20).toInt()]
val latFirst = upper[(newLatitude / 10).toInt()]
val lonSecond = ((newLongitude / 2) % 10).toInt().toString()
val latSecond = (newLatitude % 10).toInt().toString()
val lonThird = lower[((newLongitude % 2) * 12).toInt()]
val latThird = lower[((newLatitude % 1) * 24).toInt()]
return "$lonFirst$latFirst$lonSecond$latSecond$lonThird$latThird"
}
fun isValidPosition(lat: Double, lon: Double): Boolean {
private fun isValidPosition(lat: Double, lon: Double): Boolean {
return (lat >= -90.0 && lat <= 90.0) && (lon >= -180.0 && lon <= 360.0)
}
private fun isValidLocator(locator: String): Boolean {
return locator.matches("[a-xA-X][a-xA-X][0-9][0-9][a-xA-X][a-xA-X]".toRegex())
return locator.matches("[a-xA-X][a-xA-X]\\d\\d[a-xA-X][a-xA-X]".toRegex())
}
}
@@ -17,7 +17,7 @@
*/
package com.rtbishop.look4sat
import com.rtbishop.look4sat.utility.DataParser
import com.rtbishop.look4sat.data.DataParser
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.runTest