diff --git a/build.sbt b/build.sbt index 43786a5..681f61e 100644 --- a/build.sbt +++ b/build.sbt @@ -18,8 +18,10 @@ libraryDependencies ++= Seq( "ch.qos.logback" % "logback-classic" % "1.2.9", "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-extras" % circeVersion, "io.circe" %% "circe-optics" % circeVersion, diff --git a/src/main/scala/parse/AggregateData.scala b/src/main/scala/parse/AggregateData.scala new file mode 100644 index 0000000..3bc6350 --- /dev/null +++ b/src/main/scala/parse/AggregateData.scala @@ -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 +} \ No newline at end of file diff --git a/src/main/scala/parse/Main.scala b/src/main/scala/parse/Main.scala index 5abf5dc..b39d123 100644 --- a/src/main/scala/parse/Main.scala +++ b/src/main/scala/parse/Main.scala @@ -2,6 +2,7 @@ package parse import cats.effect.IO import cats.effect.unsafe.implicits.global +import io.circe.syntax.EncoderOps import java.time.LocalDateTime import java.time.format.DateTimeFormatter @@ -16,8 +17,8 @@ object Main { for { lines <- db.DBService.getInRange(from, to) - parsed <- IO.pure(Parser.queryData(lines, List("Liepāja", "Rēzekne", "randomstr"), AggregateMeteo.tempAvg)) - _ <- IO.println(parsed) + parsed <- IO.pure(Parser.queryData(lines, List("Liepāja", "Rēzekne", "randomstr"), AggregateKey.tempMax)) + _ <- IO.println(parsed.asJson) } yield () } diff --git a/src/main/scala/parse/MeteoData.scala b/src/main/scala/parse/MeteoData.scala index 3e87752..0fa275e 100644 --- a/src/main/scala/parse/MeteoData.scala +++ b/src/main/scala/parse/MeteoData.scala @@ -1,7 +1,7 @@ package parse import java.time.LocalDateTime -import scala.reflect.runtime.universe.{termNames, typeOf} +import scala.reflect.runtime.universe._ case class WeatherStationData( @@ -27,42 +27,6 @@ case class MeteoData( 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 { def fromDoubles(data: List[Option[Double]]): Option[MeteoData] = data match { case List( @@ -102,11 +66,6 @@ object MeteoData { val paramCount = constructor.paramLists.flatten.size 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") diff --git a/src/main/scala/parse/Parser.scala b/src/main/scala/parse/Parser.scala index 434ee98..22b3d54 100644 --- a/src/main/scala/parse/Parser.scala +++ b/src/main/scala/parse/Parser.scala @@ -10,6 +10,7 @@ object Parser { def parseTimestamp(timestampStr: String): Option[LocalDateTime] = { 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 { case Right(timestamp) => Some(timestamp) case Left(_) => None @@ -35,22 +36,45 @@ object Parser { private def aggregateLines( lines: List[String], cities: List[String], - aggregator: AggregateMeteo, - ): Map[String, Double] = { + aggregator: AggregateKey, + ): Map[String, AggregateValue] = { val weatherByCity = lines .flatMap(parseLine) .filter(line => cities.contains(line.city)) .groupBy(_.city) aggregator match { - case AggregateMeteo.tempAvg => weatherByCity.map { case (city, weatherData) => city -> weatherData.flatMap(_.meteo.tempMax).max } - case AggregateMeteo.tempAvg => weatherByCity.map { case (city, weatherData) => city -> weatherData.flatMap(_.meteo.tempMin).min } - case AggregateMeteo.tempAvg => weatherByCity.map { case (city, weatherData) => city -> { - val avgList = weatherData.flatMap(_.meteo.tempAvg) - avgList.sum / avgList.length + case AggregateKey.tempMax => weatherByCity.map { case (city, weatherData) => city -> + AggregateValue.tempMax(weatherData.flatMap(_.meteo.tempMax).max) } + 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) + 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 } } @@ -59,8 +83,8 @@ object Parser { def queryData( data: List[String], cities: List[String], - aggregator: AggregateMeteo, - ): Map[String, Double] = { + aggregator: AggregateKey, + ): Map[String, AggregateValue] = { aggregateLines(data, cities, aggregator) } } \ No newline at end of file diff --git a/src/main/scala/server/Server.scala b/src/main/scala/server/Server.scala index 8f21e2b..5d788b4 100644 --- a/src/main/scala/server/Server.scala +++ b/src/main/scala/server/Server.scala @@ -4,9 +4,9 @@ import cats.effect._ import cats.implicits.toTraverseOps import db.DBService import fetch.FetchService -import parse.{MeteoData, Parser} +import parse.{AggregateKey, AggregateValue, Parser} import server.ValidateRoutes.{Aggregate, CityList, DateTimeRange, ValidDate} -import io.circe.{Json, Printer} +import io.circe.{Encoder, Json, Printer} import org.http4s._ import org.http4s.dsl.io._ import org.http4s.server.Router @@ -31,6 +31,7 @@ object Server extends IOApp { DBService.getInRange(from, to) .map(Parser.queryData(_, cities, aggregate)) .flatMap(result => Ok(result.asJson.pretty)) +// .flatMap(result => Ok("make json encoder")) // http://localhost:3000/fetch/date/20230423 case GET -> Root / "fetch" / "date" / ValidDate(date) => @@ -64,7 +65,7 @@ object Server extends IOApp { // http://localhost:3000/help case GET -> Root / "help" => - Ok(MeteoData.getKeys.asJson.pretty) + Ok(AggregateKey.getKeys.asJson.pretty) } private val httpApp = Router("/" -> appRoutes).orNotFound diff --git a/src/main/scala/server/ValidateRoutes.scala b/src/main/scala/server/ValidateRoutes.scala index 9e6787e..4852aef 100644 --- a/src/main/scala/server/ValidateRoutes.scala +++ b/src/main/scala/server/ValidateRoutes.scala @@ -1,6 +1,6 @@ package server -import parse.{AggregateMeteo, Meteo} +import parse.AggregateKey import java.time.{LocalDate, LocalDateTime} import java.time.format.DateTimeFormatter @@ -36,8 +36,8 @@ object ValidateRoutes { } object Aggregate { - def unapply(str: String): Option[AggregateMeteo] = { - Meteo.stringToAggregateParam(str) + def unapply(str: String): Option[AggregateKey] = { + AggregateKey.stringToAggregateParam(str) } } }