Change data structure for meteo adding AggregateKey and AggregateValue

This commit is contained in:
Guntis Smaukstelis
2023-04-26 16:37:11 +03:00
parent 3cd223dddd
commit 1bf41973a7
7 changed files with 161 additions and 62 deletions
+3 -1
View File
@@ -18,8 +18,10 @@ libraryDependencies ++= Seq(
"ch.qos.logback" % "logback-classic" % "1.2.9", "ch.qos.logback" % "logback-classic" % "1.2.9",
"com.typesafe" % "config" % "1.4.1", "com.typesafe" % "config" % "1.4.1",
"org.scala-lang" % "scala-reflect" % "2.13.10",
"io.circe" %% "circe-core" % circeVersion,
"io.circe" %% "circe-core" % circeVersion,
"io.circe" %% "circe-generic" % circeVersion, "io.circe" %% "circe-generic" % circeVersion,
"io.circe" %% "circe-generic-extras" % circeVersion, "io.circe" %% "circe-generic-extras" % circeVersion,
"io.circe" %% "circe-optics" % circeVersion, "io.circe" %% "circe-optics" % circeVersion,
+112
View File
@@ -0,0 +1,112 @@
package parse
import io.circe._
import io.circe.syntax.EncoderOps
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
sealed trait AggregateValue
object AggregateValue {
val dateFormatter = DateTimeFormatter.ofPattern("yyyyMMdd-HHmm")
case class tempMax(value: Double) extends AggregateValue
case class tempMaxList(value: Map[LocalDateTime, Double]) extends AggregateValue
case class tempMin(value: Double) extends AggregateValue
case class tempMinList(value: Map[LocalDateTime, Double]) extends AggregateValue
case class tempAvg(value: Double) extends AggregateValue
case class tempAvgList(value: Map[LocalDateTime, Double]) extends AggregateValue
case class precipitationSum(value: Double) extends AggregateValue
case class precipitationList(value: Map[LocalDateTime, Double]) extends AggregateValue
implicit val encodeAggregateValue: Encoder[AggregateValue] = Encoder.instance {
case tm: tempMax => Json.fromDoubleOrNull(tm.value)
case tml: tempMaxList => tml.value.map { case (dateTime, value) =>
Json.obj(dateFormatter.format(dateTime) -> Json.fromDoubleOrNull(value))
}.toList.asJson
case tmin: tempMin => Json.fromDoubleOrNull(tmin.value)
case tml: tempMinList => tml.value.map { case (dateTime, value) =>
Json.obj(dateFormatter.format(dateTime) -> Json.fromDoubleOrNull(value))
}.toList.asJson
case tavg: tempAvg => Json.fromDoubleOrNull(tavg.value)
case tml: tempAvgList => tml.value.map { case (dateTime, value) =>
Json.obj(dateFormatter.format(dateTime) -> Json.fromDoubleOrNull(value))
}.toList.asJson
case psum: precipitationSum => Json.fromDoubleOrNull(psum.value)
case plist: precipitationList => plist.value.map { case (dateTime, value) =>
Json.obj(dateFormatter.format(dateTime) -> Json.fromDoubleOrNull(value))
}.toList.asJson
}
def getKeys: List[String] = {
val runtimeMirror = scala.reflect.runtime.currentMirror
val weatherParamClassSymbol = runtimeMirror.classSymbol(classOf[AggregateValue])
// Get all case classes that extend the WeatherParameter trait
val weatherParameterCases = weatherParamClassSymbol.knownDirectSubclasses.map(_.name.toString).toList
weatherParameterCases
}
}
sealed trait AggregateKey
object AggregateKey {
def stringToAggregateParam(strParam: String): Option[AggregateKey] = strParam match {
case "tempMax" => Some(AggregateKey.tempMax)
case "tempMaxList" => Some(AggregateKey.tempMaxList)
case "tempMin" => Some(AggregateKey.tempMin)
case "tempMinList" => Some(AggregateKey.tempMinList)
case "tempAvg" => Some(AggregateKey.tempAvg)
case "tempAvgList" => Some(AggregateKey.tempAvgList)
case "precipitationSum" => Some(AggregateKey.precipitationSum)
case "precipitationList" => Some(AggregateKey.precipitationList)
// case "windSpeedAvg" => Some(AggregateKey.windSpeedAvg)
// case "windGustMax" => Some(AggregateKey.windGustMax)
// case "snowThicknessAvg" => Some(AggregateKey.snowThicknessAvg)
// case "dewPointAvg" => Some(AggregateKey.dewPointAvg)
// case "airHumidityAvg" => Some(AggregateKey.airHumidityAvg)
case _ => None
}
def getKeys: List[String] = {
val runtimeMirror = scala.reflect.runtime.currentMirror
val weatherParamClassSymbol = runtimeMirror.classSymbol(classOf[AggregateKey])
// Get all case classes that extend the WeatherParameter trait
val weatherParameterCases = weatherParamClassSymbol.knownDirectSubclasses.map(_.name.toString).toList
weatherParameterCases
}
case object tempMax extends AggregateKey
case object tempMaxList extends AggregateKey
case object tempMin extends AggregateKey
case object tempMinList extends AggregateKey
case object tempAvg extends AggregateKey
case object tempAvgList extends AggregateKey
case object precipitationSum extends AggregateKey
case object precipitationList extends AggregateKey
// case object windSpeedAvg extends AggregateKey
//
// case object windGustMax extends AggregateKey
//
// case object snowThicknessAvg extends AggregateKey
//
// case object dewPointAvg extends AggregateKey
//
// case object airHumidityAvg extends AggregateKey
}
+3 -2
View File
@@ -2,6 +2,7 @@ package parse
import cats.effect.IO import cats.effect.IO
import cats.effect.unsafe.implicits.global import cats.effect.unsafe.implicits.global
import io.circe.syntax.EncoderOps
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
@@ -16,8 +17,8 @@ object Main {
for { for {
lines <- db.DBService.getInRange(from, to) lines <- db.DBService.getInRange(from, to)
parsed <- IO.pure(Parser.queryData(lines, List("Liepāja", "Rēzekne", "randomstr"), AggregateMeteo.tempAvg)) parsed <- IO.pure(Parser.queryData(lines, List("Liepāja", "Rēzekne", "randomstr"), AggregateKey.tempMax))
_ <- IO.println(parsed) _ <- IO.println(parsed.asJson)
} yield () } yield ()
} }
+1 -42
View File
@@ -1,7 +1,7 @@
package parse package parse
import java.time.LocalDateTime import java.time.LocalDateTime
import scala.reflect.runtime.universe.{termNames, typeOf} import scala.reflect.runtime.universe._
case class WeatherStationData( case class WeatherStationData(
@@ -27,42 +27,6 @@ case class MeteoData(
sunshineDuration: Option[Double], sunshineDuration: Option[Double],
) )
sealed trait AggregateMeteo
object AggregateMeteo {
case object tempMax extends AggregateMeteo
case object tempMin extends AggregateMeteo
case object tempAvg extends AggregateMeteo
case object precipitationSum extends AggregateMeteo
case object windSpeedAvg extends AggregateMeteo
case object windGustMax extends AggregateMeteo
case object snowThicknessAvg extends AggregateMeteo
case object dewPointAvg extends AggregateMeteo
case object airHumidityAvg extends AggregateMeteo
}
object Meteo {
def stringToAggregateParam(strParam: String): Option[AggregateMeteo] = strParam match {
case "tempMax" => Some(AggregateMeteo.tempMax)
case "tempMin" => Some(AggregateMeteo.tempMin)
case "tempAvg" => Some(AggregateMeteo.tempAvg)
case "precipitationSum" => Some(AggregateMeteo.precipitationSum)
case "windSpeedAvg" => Some(AggregateMeteo.windSpeedAvg)
case "windGustMax" => Some(AggregateMeteo.windGustMax)
case "snowThicknessAvg" => Some(AggregateMeteo.snowThicknessAvg)
case "dewPointAvg" => Some(AggregateMeteo.dewPointAvg)
case "airHumidityAvg" => Some(AggregateMeteo.airHumidityAvg)
case _ => None
}
}
object MeteoData { object MeteoData {
def fromDoubles(data: List[Option[Double]]): Option[MeteoData] = data match { def fromDoubles(data: List[Option[Double]]): Option[MeteoData] = data match {
case List( case List(
@@ -102,11 +66,6 @@ object MeteoData {
val paramCount = constructor.paramLists.flatten.size val paramCount = constructor.paramLists.flatten.size
paramCount paramCount
} }
def getKeys: List[String] = {
val constructor = typeOf[MeteoData].decl(termNames.CONSTRUCTOR).asMethod
constructor.paramLists.flatten.map(_.name.toString)
}
} }
//val cityList: List[String] = List("Ainaži", "Alūksne", "Bauska", "Dagda", "Daugavgrīva", "Daugavpils", "Dobele", "Gulbene", "Jelgava", "Kalnciems", "Kolka", "Kuldīga", "Lielpēči", "Liepāja", "Madona", "Mērsrags", "Pāvilosta", "Piedruja", "Priekuļi", "Rēzekne", "Rīga", "Rucava", "Rūjiena", "Saldus", "Sigulda", "Sīļi", "Skrīveri", "Skulte", "Stende", "Ventspils", "Vičaki", "Zīlāni", "Zosēni", "Ainaži", "Alūksne", "Bauska", "Dagda", "Daugavgrīva", "Daugavpils", "Dobele", "Gulbene", "Jelgava", "Kalnciems", "Kolka", "Kuldīga", "Lielpēči", "Liepāja", "Madona", "Mērsrags", "Pāvilosta", "Piedruja", "Priekuļi", "Rēzekne", "Rīga", "Rucava", "Rūjiena", "Saldus", "Sigulda", "Sīļi", "Skrīveri", "Skulte", "Stende", "Ventspils", "Vičaki", "Zīlāni", "Zosēni") //val cityList: List[String] = List("Ainaži", "Alūksne", "Bauska", "Dagda", "Daugavgrīva", "Daugavpils", "Dobele", "Gulbene", "Jelgava", "Kalnciems", "Kolka", "Kuldīga", "Lielpēči", "Liepāja", "Madona", "Mērsrags", "Pāvilosta", "Piedruja", "Priekuļi", "Rēzekne", "Rīga", "Rucava", "Rūjiena", "Saldus", "Sigulda", "Sīļi", "Skrīveri", "Skulte", "Stende", "Ventspils", "Vičaki", "Zīlāni", "Zosēni", "Ainaži", "Alūksne", "Bauska", "Dagda", "Daugavgrīva", "Daugavpils", "Dobele", "Gulbene", "Jelgava", "Kalnciems", "Kolka", "Kuldīga", "Lielpēči", "Liepāja", "Madona", "Mērsrags", "Pāvilosta", "Piedruja", "Priekuļi", "Rēzekne", "Rīga", "Rucava", "Rūjiena", "Saldus", "Sigulda", "Sīļi", "Skrīveri", "Skulte", "Stende", "Ventspils", "Vičaki", "Zīlāni", "Zosēni")
+33 -9
View File
@@ -10,6 +10,7 @@ object Parser {
def parseTimestamp(timestampStr: String): Option[LocalDateTime] = { def parseTimestamp(timestampStr: String): Option[LocalDateTime] = {
val formatter = DateTimeFormatter.ofPattern("yyyydd.MM HH:mm") val formatter = DateTimeFormatter.ofPattern("yyyydd.MM HH:mm")
// TODO figure out what to do with hardcoded year. Proly fetched data should be also modified to inlcude year
Try(LocalDateTime.parse(s"2023${timestampStr.trim}", formatter)).toEither match { Try(LocalDateTime.parse(s"2023${timestampStr.trim}", formatter)).toEither match {
case Right(timestamp) => Some(timestamp) case Right(timestamp) => Some(timestamp)
case Left(_) => None case Left(_) => None
@@ -35,22 +36,45 @@ object Parser {
private def aggregateLines( private def aggregateLines(
lines: List[String], lines: List[String],
cities: List[String], cities: List[String],
aggregator: AggregateMeteo, aggregator: AggregateKey,
): Map[String, Double] = { ): Map[String, AggregateValue] = {
val weatherByCity = lines val weatherByCity = lines
.flatMap(parseLine) .flatMap(parseLine)
.filter(line => cities.contains(line.city)) .filter(line => cities.contains(line.city))
.groupBy(_.city) .groupBy(_.city)
aggregator match { aggregator match {
case AggregateMeteo.tempAvg => weatherByCity.map { case (city, weatherData) => city -> weatherData.flatMap(_.meteo.tempMax).max } case AggregateKey.tempMax => weatherByCity.map { case (city, weatherData) => city ->
case AggregateMeteo.tempAvg => weatherByCity.map { case (city, weatherData) => city -> weatherData.flatMap(_.meteo.tempMin).min } AggregateValue.tempMax(weatherData.flatMap(_.meteo.tempMax).max) }
case AggregateMeteo.tempAvg => weatherByCity.map { case (city, weatherData) => city -> { case AggregateKey.tempMaxList => weatherByCity.map { case (city, weatherData) => city ->
AggregateValue.tempMaxList(weatherData.collect {
case wd if wd.meteo.tempMax.isDefined => wd.timestamp -> wd.meteo.tempMax.get
}.toMap) }
case AggregateKey.tempMin => weatherByCity.map { case (city, weatherData) => city ->
AggregateValue.tempMin(weatherData.flatMap(_.meteo.tempMin).min) }
case AggregateKey.tempMinList => weatherByCity.map { case (city, weatherData) => city ->
AggregateValue.tempMinList(weatherData.collect {
case wd if wd.meteo.tempMin.isDefined => wd.timestamp -> wd.meteo.tempMin.get
}.toMap)}
case AggregateKey.tempAvg => weatherByCity.map { case (city, weatherData) => city ->
AggregateValue.tempAvg({
val avgList = weatherData.flatMap(_.meteo.tempAvg) val avgList = weatherData.flatMap(_.meteo.tempAvg)
avgList.sum / avgList.length avgList.sum / avgList.length
})}
case AggregateKey.tempAvgList => weatherByCity.map { case (city, weatherData) => city ->
AggregateValue.tempAvgList(weatherData.collect {
case wd if wd.meteo.tempAvg.isDefined => wd.timestamp -> wd.meteo.tempAvg.get
}.toMap)}
case AggregateKey.precipitationSum => weatherByCity.map { case (city, weatherData) => city ->
AggregateValue.precipitationSum(weatherData.flatMap(_.meteo.precipitation).sum)}
case AggregateKey.precipitationList => weatherByCity.map { case (city, weatherData) => city ->
AggregateValue.precipitationList(weatherData.collect {
case wd if wd.meteo.precipitation.isDefined => wd.timestamp -> wd.meteo.precipitation.get
}.toMap)
} }
}
case AggregateMeteo.precipitationSum => weatherByCity.map { case (city, weatherData) => city -> weatherData.flatMap(_.meteo.precipitation).sum }
// TODO add here other aggregateParams // TODO add here other aggregateParams
} }
} }
@@ -59,8 +83,8 @@ object Parser {
def queryData( def queryData(
data: List[String], data: List[String],
cities: List[String], cities: List[String],
aggregator: AggregateMeteo, aggregator: AggregateKey,
): Map[String, Double] = { ): Map[String, AggregateValue] = {
aggregateLines(data, cities, aggregator) aggregateLines(data, cities, aggregator)
} }
} }
+4 -3
View File
@@ -4,9 +4,9 @@ import cats.effect._
import cats.implicits.toTraverseOps import cats.implicits.toTraverseOps
import db.DBService import db.DBService
import fetch.FetchService import fetch.FetchService
import parse.{MeteoData, Parser} import parse.{AggregateKey, AggregateValue, Parser}
import server.ValidateRoutes.{Aggregate, CityList, DateTimeRange, ValidDate} import server.ValidateRoutes.{Aggregate, CityList, DateTimeRange, ValidDate}
import io.circe.{Json, Printer} import io.circe.{Encoder, Json, Printer}
import org.http4s._ import org.http4s._
import org.http4s.dsl.io._ import org.http4s.dsl.io._
import org.http4s.server.Router import org.http4s.server.Router
@@ -31,6 +31,7 @@ object Server extends IOApp {
DBService.getInRange(from, to) DBService.getInRange(from, to)
.map(Parser.queryData(_, cities, aggregate)) .map(Parser.queryData(_, cities, aggregate))
.flatMap(result => Ok(result.asJson.pretty)) .flatMap(result => Ok(result.asJson.pretty))
// .flatMap(result => Ok("make json encoder"))
// http://localhost:3000/fetch/date/20230423 // http://localhost:3000/fetch/date/20230423
case GET -> Root / "fetch" / "date" / ValidDate(date) => case GET -> Root / "fetch" / "date" / ValidDate(date) =>
@@ -64,7 +65,7 @@ object Server extends IOApp {
// http://localhost:3000/help // http://localhost:3000/help
case GET -> Root / "help" => case GET -> Root / "help" =>
Ok(MeteoData.getKeys.asJson.pretty) Ok(AggregateKey.getKeys.asJson.pretty)
} }
private val httpApp = Router("/" -> appRoutes).orNotFound private val httpApp = Router("/" -> appRoutes).orNotFound
+3 -3
View File
@@ -1,6 +1,6 @@
package server package server
import parse.{AggregateMeteo, Meteo} import parse.AggregateKey
import java.time.{LocalDate, LocalDateTime} import java.time.{LocalDate, LocalDateTime}
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
@@ -36,8 +36,8 @@ object ValidateRoutes {
} }
object Aggregate { object Aggregate {
def unapply(str: String): Option[AggregateMeteo] = { def unapply(str: String): Option[AggregateKey] = {
Meteo.stringToAggregateParam(str) AggregateKey.stringToAggregateParam(str)
} }
} }
} }