Change data structure for meteo adding AggregateKey and AggregateValue
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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,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")
|
||||||
|
|||||||
@@ -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 ->
|
||||||
val avgList = weatherData.flatMap(_.meteo.tempAvg)
|
AggregateValue.tempMaxList(weatherData.collect {
|
||||||
avgList.sum / avgList.length
|
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)
|
||||||
|
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,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
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user