Code refactoring + made anomaly detection system more stable.
Some checks failed
Docker / docker (push) Has been cancelled
Tests / build (push) Has been cancelled
Some checks failed
Docker / docker (push) Has been cancelled
Tests / build (push) Has been cancelled
This commit is contained in:
80
cache/master.go
vendored
Normal file
80
cache/master.go
vendored
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
package cache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ceticamarco/zephyr/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// cacheType, representing the abstract value of a CacheEntity
|
||||||
|
type cacheType interface {
|
||||||
|
types.Weather | types.Metrics | types.Wind | types.DailyForecast | types.HourlyForecast | types.Moon
|
||||||
|
}
|
||||||
|
|
||||||
|
// CacheEntity, representing the value of the cache
|
||||||
|
type CacheEntity[T cacheType] struct {
|
||||||
|
element T
|
||||||
|
timestamp time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// MasterCache, representing a mapping between a key(str) and a CacheEntity
|
||||||
|
type MasterCache[T cacheType] struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
Data map[string]CacheEntity[T]
|
||||||
|
}
|
||||||
|
|
||||||
|
// MasterCaches, representing a grouping of the various caches
|
||||||
|
type MasterCaches struct {
|
||||||
|
WeatherCache MasterCache[types.Weather]
|
||||||
|
MetricsCache MasterCache[types.Metrics]
|
||||||
|
WindCache MasterCache[types.Wind]
|
||||||
|
DailyForecastCache MasterCache[types.DailyForecast]
|
||||||
|
HourlyForecastCache MasterCache[types.HourlyForecast]
|
||||||
|
MoonCache MasterCache[types.Moon]
|
||||||
|
}
|
||||||
|
|
||||||
|
func InitMasterCache() *MasterCaches {
|
||||||
|
return &MasterCaches{
|
||||||
|
WeatherCache: MasterCache[types.Weather]{Data: make(map[string]CacheEntity[types.Weather])},
|
||||||
|
MetricsCache: MasterCache[types.Metrics]{Data: make(map[string]CacheEntity[types.Metrics])},
|
||||||
|
WindCache: MasterCache[types.Wind]{Data: make(map[string]CacheEntity[types.Wind])},
|
||||||
|
DailyForecastCache: MasterCache[types.DailyForecast]{Data: make(map[string]CacheEntity[types.DailyForecast])},
|
||||||
|
HourlyForecastCache: MasterCache[types.HourlyForecast]{Data: make(map[string]CacheEntity[types.HourlyForecast])},
|
||||||
|
MoonCache: MasterCache[types.Moon]{Data: make(map[string]CacheEntity[types.Moon])},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cache *MasterCache[T]) GetEntry(cityName string, ttl int8) (T, bool) {
|
||||||
|
cache.mu.RLock()
|
||||||
|
defer cache.mu.RUnlock()
|
||||||
|
|
||||||
|
val, isPresent := cache.Data[strings.ToUpper(cityName)]
|
||||||
|
|
||||||
|
// If key is not present, return a zero value
|
||||||
|
if !isPresent {
|
||||||
|
return val.element, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise check whether cache element is expired
|
||||||
|
currentTime := time.Now()
|
||||||
|
expired := currentTime.Sub(val.timestamp) > (time.Duration(ttl) * time.Hour)
|
||||||
|
if expired {
|
||||||
|
return val.element, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return val.element, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cache *MasterCache[T]) AddEntry(entry T, cityName string) {
|
||||||
|
cache.mu.Lock()
|
||||||
|
defer cache.mu.Unlock()
|
||||||
|
|
||||||
|
currentTime := time.Now()
|
||||||
|
|
||||||
|
cache.Data[strings.ToUpper(cityName)] = CacheEntity[T]{
|
||||||
|
element: entry,
|
||||||
|
timestamp: currentTime,
|
||||||
|
}
|
||||||
|
}
|
||||||
93
cache/statistics.go
vendored
Normal file
93
cache/statistics.go
vendored
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
package cache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ceticamarco/zephyr/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// statistic cache data type, representing a mapping between a location+date and its daily average temperature
|
||||||
|
type StatCache struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
db map[string]float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func InitStatCache() *StatCache {
|
||||||
|
return &StatCache{
|
||||||
|
db: make(map[string]float64),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cache *StatCache) AddStatistic(cityName string, statDate string, dailyTemp float64) {
|
||||||
|
cache.mu.Lock()
|
||||||
|
defer cache.mu.Unlock()
|
||||||
|
|
||||||
|
// Format key as '<DATE>@<LOCATION>
|
||||||
|
key := fmt.Sprintf("%s@%s", statDate, cityName)
|
||||||
|
|
||||||
|
// Insert weather statistic into the database if it doesn't already exist
|
||||||
|
if _, exists := cache.db[key]; exists {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cache.db[key] = dailyTemp
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cache *StatCache) IsKeyInvalid(key string) bool {
|
||||||
|
cache.mu.RLock()
|
||||||
|
defer cache.mu.RUnlock()
|
||||||
|
|
||||||
|
// A key is invalid if it has less than 2 entries within the last 2 days
|
||||||
|
threshold := time.Now().AddDate(0, 0, -2)
|
||||||
|
|
||||||
|
var validEntries uint = 0
|
||||||
|
for storedKey := range cache.db {
|
||||||
|
if !strings.HasSuffix(storedKey, key) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get <DATE> from <DATE>@<LOCATION>
|
||||||
|
keyDate, err := time.Parse("2006-01-02", strings.Split(storedKey, "@")[0])
|
||||||
|
if err != nil {
|
||||||
|
keyDate = time.Now() // Add a fallback date if parsing fails
|
||||||
|
}
|
||||||
|
|
||||||
|
if !keyDate.Before(threshold) {
|
||||||
|
validEntries++
|
||||||
|
|
||||||
|
// Early skip if we already found two valid entries
|
||||||
|
if validEntries >= 2 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cache *StatCache) GetCityStatistics(cityName string) []types.StatElement {
|
||||||
|
cache.mu.RLock()
|
||||||
|
defer cache.mu.RUnlock()
|
||||||
|
|
||||||
|
result := make([]types.StatElement, 0)
|
||||||
|
|
||||||
|
for key, record := range cache.db {
|
||||||
|
if strings.HasSuffix(key, cityName) {
|
||||||
|
// Get <DATE> from <DATE>@<LOCATION>
|
||||||
|
keyDate, err := time.Parse("2006-01-02", strings.Split(key, "@")[0])
|
||||||
|
if err != nil {
|
||||||
|
keyDate = time.Now() // Add a fallback date if parsing fails
|
||||||
|
}
|
||||||
|
|
||||||
|
result = append(result, types.StatElement{
|
||||||
|
Temperature: record,
|
||||||
|
Date: keyDate,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -4,10 +4,13 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ceticamarco/zephyr/cache"
|
||||||
"github.com/ceticamarco/zephyr/model"
|
"github.com/ceticamarco/zephyr/model"
|
||||||
"github.com/ceticamarco/zephyr/types"
|
"github.com/ceticamarco/zephyr/types"
|
||||||
)
|
)
|
||||||
@@ -102,7 +105,7 @@ func deepCopyForecast[T types.DailyForecast | types.HourlyForecast](original T)
|
|||||||
return fc_copy
|
return fc_copy
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetWeather(res http.ResponseWriter, req *http.Request, cache *types.Cache[types.Weather], statDB *types.StatDB, vars *types.Variables) {
|
func GetWeather(res http.ResponseWriter, req *http.Request, cache *cache.MasterCache[types.Weather], statCache *cache.StatCache, vars *types.Variables) {
|
||||||
if req.Method != http.MethodGet {
|
if req.Method != http.MethodGet {
|
||||||
jsonError(res, "error", "method not allowed", http.StatusMethodNotAllowed)
|
jsonError(res, "error", "method not allowed", http.StatusMethodNotAllowed)
|
||||||
return
|
return
|
||||||
@@ -138,7 +141,7 @@ func GetWeather(res http.ResponseWriter, req *http.Request, cache *types.Cache[t
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get city weather
|
// Get city weather
|
||||||
weather, err := model.GetWeather(&city, vars.Token)
|
weather, dailyTemp, err := model.GetWeather(&city, vars.Token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
jsonError(res, "error", err.Error(), http.StatusBadRequest)
|
jsonError(res, "error", err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
@@ -148,7 +151,8 @@ func GetWeather(res http.ResponseWriter, req *http.Request, cache *types.Cache[t
|
|||||||
cache.AddEntry(weather, fmtKey(cityName))
|
cache.AddEntry(weather, fmtKey(cityName))
|
||||||
|
|
||||||
// Insert new statistic entry into the statistics database
|
// Insert new statistic entry into the statistics database
|
||||||
statDB.AddStatistic(fmtKey(cityName), weather)
|
currentDate := time.Now().Format("2006-01-02")
|
||||||
|
statCache.AddStatistic(fmtKey(cityName), currentDate, dailyTemp)
|
||||||
|
|
||||||
// Format weather object and then return it
|
// Format weather object and then return it
|
||||||
weather.Temperature = fmtTemperature(weather.Temperature, isImperial)
|
weather.Temperature = fmtTemperature(weather.Temperature, isImperial)
|
||||||
@@ -160,7 +164,7 @@ func GetWeather(res http.ResponseWriter, req *http.Request, cache *types.Cache[t
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetMetrics(res http.ResponseWriter, req *http.Request, cache *types.Cache[types.Metrics], vars *types.Variables) {
|
func GetMetrics(res http.ResponseWriter, req *http.Request, cache *cache.MasterCache[types.Metrics], vars *types.Variables) {
|
||||||
if req.Method != http.MethodGet {
|
if req.Method != http.MethodGet {
|
||||||
jsonError(res, "error", "method not allowed", http.StatusMethodNotAllowed)
|
jsonError(res, "error", "method not allowed", http.StatusMethodNotAllowed)
|
||||||
return
|
return
|
||||||
@@ -215,7 +219,7 @@ func GetMetrics(res http.ResponseWriter, req *http.Request, cache *types.Cache[t
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetWind(res http.ResponseWriter, req *http.Request, cache *types.Cache[types.Wind], vars *types.Variables) {
|
func GetWind(res http.ResponseWriter, req *http.Request, cache *cache.MasterCache[types.Wind], vars *types.Variables) {
|
||||||
if req.Method != http.MethodGet {
|
if req.Method != http.MethodGet {
|
||||||
jsonError(res, "error", "method not allowed", http.StatusMethodNotAllowed)
|
jsonError(res, "error", "method not allowed", http.StatusMethodNotAllowed)
|
||||||
return
|
return
|
||||||
@@ -267,8 +271,8 @@ func GetWind(res http.ResponseWriter, req *http.Request, cache *types.Cache[type
|
|||||||
func GetForecast(
|
func GetForecast(
|
||||||
res http.ResponseWriter,
|
res http.ResponseWriter,
|
||||||
req *http.Request,
|
req *http.Request,
|
||||||
dCache *types.Cache[types.DailyForecast],
|
dCache *cache.MasterCache[types.DailyForecast],
|
||||||
hCache *types.Cache[types.HourlyForecast],
|
hCache *cache.MasterCache[types.HourlyForecast],
|
||||||
vars *types.Variables,
|
vars *types.Variables,
|
||||||
) {
|
) {
|
||||||
if req.Method != http.MethodGet {
|
if req.Method != http.MethodGet {
|
||||||
@@ -340,7 +344,7 @@ func GetForecast(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetMoon(res http.ResponseWriter, req *http.Request, cache *types.Cache[types.Moon], vars *types.Variables) {
|
func GetMoon(res http.ResponseWriter, req *http.Request, cache *cache.MasterCache[types.Moon], vars *types.Variables) {
|
||||||
if req.Method != http.MethodGet {
|
if req.Method != http.MethodGet {
|
||||||
jsonError(res, "error", "method not allowed", http.StatusMethodNotAllowed)
|
jsonError(res, "error", "method not allowed", http.StatusMethodNotAllowed)
|
||||||
return
|
return
|
||||||
@@ -370,7 +374,23 @@ func GetMoon(res http.ResponseWriter, req *http.Request, cache *types.Cache[type
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetStatistics(res http.ResponseWriter, req *http.Request, statDB *types.StatDB) {
|
func addRandomStatistics(statDB *cache.StatCache, city string, n int, meanTemp, stdDev float64) {
|
||||||
|
now := time.Now().AddDate(0, 0, -1) // Start from yesterday
|
||||||
|
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||||
|
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
date := now.AddDate(0, 0, -i)
|
||||||
|
temp := r.NormFloat64()*stdDev + meanTemp
|
||||||
|
|
||||||
|
statDB.AddStatistic(
|
||||||
|
fmtKey(city),
|
||||||
|
date.Format("2006-01-02"),
|
||||||
|
temp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetStatistics(res http.ResponseWriter, req *http.Request, statCache *cache.StatCache) {
|
||||||
if req.Method != http.MethodGet {
|
if req.Method != http.MethodGet {
|
||||||
jsonError(res, "error", "method not allowed", http.StatusMethodNotAllowed)
|
jsonError(res, "error", "method not allowed", http.StatusMethodNotAllowed)
|
||||||
return
|
return
|
||||||
@@ -389,7 +409,7 @@ func GetStatistics(res http.ResponseWriter, req *http.Request, statDB *types.Sta
|
|||||||
isImperial := req.URL.Query().Has("i")
|
isImperial := req.URL.Query().Has("i")
|
||||||
|
|
||||||
// Get city statistics
|
// Get city statistics
|
||||||
stats, err := model.GetStatistics(fmtKey(cityName), statDB)
|
stats, err := model.GetStatistics(fmtKey(cityName), statCache)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
jsonError(res, "error", err.Error(), http.StatusBadRequest)
|
jsonError(res, "error", err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
|
|||||||
17
main.go
17
main.go
@@ -7,6 +7,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/ceticamarco/zephyr/cache"
|
||||||
"github.com/ceticamarco/zephyr/controller"
|
"github.com/ceticamarco/zephyr/controller"
|
||||||
"github.com/ceticamarco/zephyr/types"
|
"github.com/ceticamarco/zephyr/types"
|
||||||
)
|
)
|
||||||
@@ -25,8 +26,8 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Initialize cache, statDB and vars
|
// Initialize cache, statDB and vars
|
||||||
cache := types.InitCache()
|
masterCache := cache.InitMasterCache()
|
||||||
statDB := types.InitDB()
|
statCache := cache.InitStatCache()
|
||||||
vars := types.Variables{
|
vars := types.Variables{
|
||||||
Token: token,
|
Token: token,
|
||||||
TimeToLive: int8(ttl),
|
TimeToLive: int8(ttl),
|
||||||
@@ -34,27 +35,27 @@ func main() {
|
|||||||
|
|
||||||
// API endpoints
|
// API endpoints
|
||||||
http.HandleFunc("/weather/", func(res http.ResponseWriter, req *http.Request) {
|
http.HandleFunc("/weather/", func(res http.ResponseWriter, req *http.Request) {
|
||||||
controller.GetWeather(res, req, &cache.WeatherCache, statDB, &vars)
|
controller.GetWeather(res, req, &masterCache.WeatherCache, statCache, &vars)
|
||||||
})
|
})
|
||||||
|
|
||||||
http.HandleFunc("/metrics/", func(res http.ResponseWriter, req *http.Request) {
|
http.HandleFunc("/metrics/", func(res http.ResponseWriter, req *http.Request) {
|
||||||
controller.GetMetrics(res, req, &cache.MetricsCache, &vars)
|
controller.GetMetrics(res, req, &masterCache.MetricsCache, &vars)
|
||||||
})
|
})
|
||||||
|
|
||||||
http.HandleFunc("/wind/", func(res http.ResponseWriter, req *http.Request) {
|
http.HandleFunc("/wind/", func(res http.ResponseWriter, req *http.Request) {
|
||||||
controller.GetWind(res, req, &cache.WindCache, &vars)
|
controller.GetWind(res, req, &masterCache.WindCache, &vars)
|
||||||
})
|
})
|
||||||
|
|
||||||
http.HandleFunc("/forecast/", func(res http.ResponseWriter, req *http.Request) {
|
http.HandleFunc("/forecast/", func(res http.ResponseWriter, req *http.Request) {
|
||||||
controller.GetForecast(res, req, &cache.DailyForecastCache, &cache.HourlyForecastCache, &vars)
|
controller.GetForecast(res, req, &masterCache.DailyForecastCache, &masterCache.HourlyForecastCache, &vars)
|
||||||
})
|
})
|
||||||
|
|
||||||
http.HandleFunc("/moon", func(res http.ResponseWriter, req *http.Request) {
|
http.HandleFunc("/moon", func(res http.ResponseWriter, req *http.Request) {
|
||||||
controller.GetMoon(res, req, &cache.MoonCache, &vars)
|
controller.GetMoon(res, req, &masterCache.MoonCache, &vars)
|
||||||
})
|
})
|
||||||
|
|
||||||
http.HandleFunc("/stats/", func(res http.ResponseWriter, req *http.Request) {
|
http.HandleFunc("/stats/", func(res http.ResponseWriter, req *http.Request) {
|
||||||
controller.GetStatistics(res, req, statDB)
|
controller.GetStatistics(res, req, statCache)
|
||||||
})
|
})
|
||||||
|
|
||||||
listenAddr := fmt.Sprintf("%s:%s", host, port)
|
listenAddr := fmt.Sprintf("%s:%s", host, port)
|
||||||
|
|||||||
@@ -5,37 +5,23 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/ceticamarco/zephyr/cache"
|
||||||
"github.com/ceticamarco/zephyr/statistics"
|
"github.com/ceticamarco/zephyr/statistics"
|
||||||
"github.com/ceticamarco/zephyr/types"
|
"github.com/ceticamarco/zephyr/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetStatistics(cityName string, statDB *types.StatDB) (types.StatResult, error) {
|
func GetStatistics(cityName string, statCache *cache.StatCache) (types.StatResult, error) {
|
||||||
// Check whether there are sufficient and updated records for the given location
|
// Check whether there are sufficient and updated records for the given location
|
||||||
if statDB.IsKeyInvalid(cityName) {
|
if statCache.IsKeyInvalid(cityName) {
|
||||||
return types.StatResult{}, errors.New("insufficient or outdated data to perform statistical analysis")
|
return types.StatResult{}, errors.New("insufficient or outdated data to perform statistical analysis")
|
||||||
}
|
}
|
||||||
|
|
||||||
extractTemps := func(weatherArr []types.Weather) ([]float64, error) {
|
|
||||||
temps := make([]float64, 0, len(weatherArr))
|
|
||||||
|
|
||||||
for _, weather := range weatherArr {
|
|
||||||
temperature, err := strconv.ParseFloat(weather.Temperature, 64)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
temps = append(temps, temperature)
|
|
||||||
}
|
|
||||||
|
|
||||||
return temps, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract records from the database
|
// Extract records from the database
|
||||||
stats := statDB.GetCityStatistics(cityName)
|
stats := statCache.GetCityStatistics(cityName)
|
||||||
|
// Extract temperatures from statistics
|
||||||
// Extract temperatures from weather statistics
|
temps := make([]float64, len(stats))
|
||||||
temps, err := extractTemps(stats)
|
for idx, stat := range stats {
|
||||||
if err != nil {
|
temps[idx] = stat.Temperature
|
||||||
return types.StatResult{}, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detect anomalies
|
// Detect anomalies
|
||||||
|
|||||||
@@ -42,10 +42,10 @@ func GetEmoji(condition string, isNight bool) string {
|
|||||||
return "❓"
|
return "❓"
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetWeather(city *types.City, apiKey string) (types.Weather, error) {
|
func GetWeather(city *types.City, apiKey string) (types.Weather, float64, error) {
|
||||||
url, err := url.Parse(WTR_URL)
|
url, err := url.Parse(WTR_URL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return types.Weather{}, err
|
return types.Weather{}, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
params := url.Query()
|
params := url.Query()
|
||||||
@@ -59,7 +59,7 @@ func GetWeather(city *types.City, apiKey string) (types.Weather, error) {
|
|||||||
|
|
||||||
res, err := http.Get(url.String())
|
res, err := http.Get(url.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return types.Weather{}, err
|
return types.Weather{}, 0, err
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
|
|
||||||
@@ -77,8 +77,9 @@ func GetWeather(city *types.City, apiKey string) (types.Weather, error) {
|
|||||||
} `json:"current"`
|
} `json:"current"`
|
||||||
Daily []struct {
|
Daily []struct {
|
||||||
Temp struct {
|
Temp struct {
|
||||||
Min float64 `json:"min"`
|
Daily float64 `json:"day"`
|
||||||
Max float64 `json:"max"`
|
Min float64 `json:"min"`
|
||||||
|
Max float64 `json:"max"`
|
||||||
} `json:"temp"`
|
} `json:"temp"`
|
||||||
} `json:"daily"`
|
} `json:"daily"`
|
||||||
Alerts []struct {
|
Alerts []struct {
|
||||||
@@ -91,7 +92,7 @@ func GetWeather(city *types.City, apiKey string) (types.Weather, error) {
|
|||||||
|
|
||||||
var weather WeatherRes
|
var weather WeatherRes
|
||||||
if err := json.NewDecoder(res.Body).Decode(&weather); err != nil {
|
if err := json.NewDecoder(res.Body).Decode(&weather); err != nil {
|
||||||
return types.Weather{}, err
|
return types.Weather{}, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format UNIX timestamp as 'YYYY-MM-DD'
|
// Format UNIX timestamp as 'YYYY-MM-DD'
|
||||||
@@ -143,5 +144,5 @@ func GetWeather(city *types.City, apiKey string) (types.Weather, error) {
|
|||||||
Condition: weather.Current.Weather[0].Title,
|
Condition: weather.Current.Weather[0].Title,
|
||||||
Emoji: emoji,
|
Emoji: emoji,
|
||||||
Alerts: alerts,
|
Alerts: alerts,
|
||||||
}, nil
|
}, weather.Daily[0].Temp.Daily, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -147,12 +147,10 @@ func RobustZScore(temperatures []float64) []struct {
|
|||||||
return anomalies
|
return anomalies
|
||||||
}
|
}
|
||||||
|
|
||||||
func DetectAnomalies(weatherArr []types.Weather) []types.WeatherAnomaly {
|
func DetectAnomalies(statsArr []types.StatElement) []types.WeatherAnomaly {
|
||||||
temps := make([]float64, len(weatherArr))
|
temps := make([]float64, len(statsArr))
|
||||||
|
for idx, stat := range statsArr {
|
||||||
for idx, weather := range weatherArr {
|
temps[idx] = stat.Temperature
|
||||||
temp, _ := strconv.ParseFloat(weather.Temperature, 64)
|
|
||||||
temps[idx] = temp
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply the Robust/MAD Z-Score anomaly detection algorithm
|
// Apply the Robust/MAD Z-Score anomaly detection algorithm
|
||||||
@@ -160,7 +158,7 @@ func DetectAnomalies(weatherArr []types.Weather) []types.WeatherAnomaly {
|
|||||||
result := make([]types.WeatherAnomaly, 0, len(anomalies))
|
result := make([]types.WeatherAnomaly, 0, len(anomalies))
|
||||||
for _, anomaly := range anomalies {
|
for _, anomaly := range anomalies {
|
||||||
result = append(result, types.WeatherAnomaly{
|
result = append(result, types.WeatherAnomaly{
|
||||||
Date: weatherArr[anomaly.Idx].Date,
|
Date: types.ZephyrDate{Date: statsArr[anomaly.Idx].Date},
|
||||||
Temp: strconv.FormatFloat(anomaly.Value, 'f', -1, 64),
|
Temp: strconv.FormatFloat(anomaly.Value, 'f', -1, 64),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,78 +0,0 @@
|
|||||||
package types
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// cacheType, representing the abstract value of a CacheEntity
|
|
||||||
type cacheType interface {
|
|
||||||
Weather | Metrics | Wind | DailyForecast | HourlyForecast | Moon
|
|
||||||
}
|
|
||||||
|
|
||||||
// CacheEntity, representing the value of the cache
|
|
||||||
type CacheEntity[T cacheType] struct {
|
|
||||||
element T
|
|
||||||
timestamp time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cache, representing a mapping between a key(str) and a CacheEntity
|
|
||||||
type Cache[T cacheType] struct {
|
|
||||||
mu sync.RWMutex
|
|
||||||
Data map[string]CacheEntity[T]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Caches, representing a grouping of the various caches
|
|
||||||
type Caches struct {
|
|
||||||
WeatherCache Cache[Weather]
|
|
||||||
MetricsCache Cache[Metrics]
|
|
||||||
WindCache Cache[Wind]
|
|
||||||
DailyForecastCache Cache[DailyForecast]
|
|
||||||
HourlyForecastCache Cache[HourlyForecast]
|
|
||||||
MoonCache Cache[Moon]
|
|
||||||
}
|
|
||||||
|
|
||||||
func InitCache() *Caches {
|
|
||||||
return &Caches{
|
|
||||||
WeatherCache: Cache[Weather]{Data: make(map[string]CacheEntity[Weather])},
|
|
||||||
MetricsCache: Cache[Metrics]{Data: make(map[string]CacheEntity[Metrics])},
|
|
||||||
WindCache: Cache[Wind]{Data: make(map[string]CacheEntity[Wind])},
|
|
||||||
DailyForecastCache: Cache[DailyForecast]{Data: make(map[string]CacheEntity[DailyForecast])},
|
|
||||||
HourlyForecastCache: Cache[HourlyForecast]{Data: make(map[string]CacheEntity[HourlyForecast])},
|
|
||||||
MoonCache: Cache[Moon]{Data: make(map[string]CacheEntity[Moon])},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cache *Cache[T]) GetEntry(cityName string, ttl int8) (T, bool) {
|
|
||||||
cache.mu.RLock()
|
|
||||||
defer cache.mu.RUnlock()
|
|
||||||
|
|
||||||
val, isPresent := cache.Data[strings.ToUpper(cityName)]
|
|
||||||
|
|
||||||
// If key is not present, return a zero value
|
|
||||||
if !isPresent {
|
|
||||||
return val.element, false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise check whether cache element is expired
|
|
||||||
currentTime := time.Now()
|
|
||||||
expired := currentTime.Sub(val.timestamp) > (time.Duration(ttl) * time.Hour)
|
|
||||||
if expired {
|
|
||||||
return val.element, false
|
|
||||||
}
|
|
||||||
|
|
||||||
return val.element, true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cache *Cache[T]) AddEntry(entry T, cityName string) {
|
|
||||||
cache.mu.Lock()
|
|
||||||
defer cache.mu.Unlock()
|
|
||||||
|
|
||||||
currentTime := time.Now()
|
|
||||||
|
|
||||||
cache.Data[strings.ToUpper(cityName)] = CacheEntity[T]{
|
|
||||||
element: entry,
|
|
||||||
timestamp: currentTime,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package types
|
|
||||||
|
|
||||||
// The City data type, representing the name, the latitude and the longitude
|
|
||||||
// of a location
|
|
||||||
type City struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Lat float64 `json:"lat"`
|
|
||||||
Lon float64 `json:"lon"`
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
package types
|
|
||||||
|
|
||||||
// The DailyForecastEntity data type, representing the weather forecast
|
|
||||||
// of a single day
|
|
||||||
type DailyForecastEntity struct {
|
|
||||||
Date ZephyrDate `json:"date"`
|
|
||||||
Min string `json:"min"`
|
|
||||||
Max string `json:"max"`
|
|
||||||
Condition string `json:"condition"`
|
|
||||||
Emoji string `json:"emoji"`
|
|
||||||
FeelsLike string `json:"feelsLike"`
|
|
||||||
Wind Wind `json:"wind"`
|
|
||||||
RainProb string `json:"rainProbability"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// The DailyForecast data type, representing a set of DailyForecastEntity
|
|
||||||
type DailyForecast struct {
|
|
||||||
Forecast []DailyForecastEntity `json:"forecast"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// The HourlyForecastEntity data type, representing the weather forecast
|
|
||||||
// of a single hour
|
|
||||||
type HourlyForecastEntity struct {
|
|
||||||
Time ZephyrTime `json:"time"`
|
|
||||||
Temperature string `json:"temperature"`
|
|
||||||
Condition string `json:"condition"`
|
|
||||||
Emoji string `json:"emoji"`
|
|
||||||
Wind Wind `json:"wind"`
|
|
||||||
RainProb string `json:"rainProbability"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// The HourlyForecast data type, representing a set of HourlyForecastEntity
|
|
||||||
type HourlyForecast struct {
|
|
||||||
Forecast []HourlyForecastEntity `json:"forecast"`
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
package types
|
|
||||||
|
|
||||||
// The Metrics data type, representing the humidity, pressure and
|
|
||||||
// similar miscellaneous values
|
|
||||||
type Metrics struct {
|
|
||||||
Humidity string `json:"humidity"`
|
|
||||||
Pressure string `json:"pressure"`
|
|
||||||
DewPoint string `json:"dewPoint"`
|
|
||||||
UvIndex string `json:"uvIndex"`
|
|
||||||
Visibility string `json:"visibility"`
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package types
|
|
||||||
|
|
||||||
// The Moon data type, representing the moon phase,
|
|
||||||
// the moon phase icon and the moon progress(%).
|
|
||||||
type Moon struct {
|
|
||||||
Icon string `json:"icon"`
|
|
||||||
Phase string `json:"phase"`
|
|
||||||
Percentage string `json:"percentage"`
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
package types
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// StatDB data type, representing a mapping between a location and its weather
|
|
||||||
type StatDB struct {
|
|
||||||
mu sync.RWMutex
|
|
||||||
db map[string]Weather
|
|
||||||
}
|
|
||||||
|
|
||||||
func InitDB() *StatDB {
|
|
||||||
return &StatDB{
|
|
||||||
db: make(map[string]Weather),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (statDB *StatDB) AddStatistic(cityName string, weather Weather) {
|
|
||||||
statDB.mu.Lock()
|
|
||||||
defer statDB.mu.Unlock()
|
|
||||||
|
|
||||||
key := fmt.Sprintf("%s@%s", weather.Date.Date.Format("2006-01-02"), cityName)
|
|
||||||
|
|
||||||
// Insert weather statistic into the database only if it isn't present
|
|
||||||
if _, isPresent := statDB.db[key]; isPresent {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
statDB.db[key] = weather
|
|
||||||
}
|
|
||||||
|
|
||||||
func (statDB *StatDB) IsKeyInvalid(key string) bool {
|
|
||||||
statDB.mu.RLock()
|
|
||||||
defer statDB.mu.RUnlock()
|
|
||||||
|
|
||||||
// A key is invalid if it has less than 2 entries within the last 2 days
|
|
||||||
threshold := time.Now().AddDate(0, 0, -2)
|
|
||||||
|
|
||||||
var validKeys uint = 0
|
|
||||||
for storedKey, record := range statDB.db {
|
|
||||||
if !strings.HasSuffix(storedKey, key) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if !record.Date.Date.Before(threshold) {
|
|
||||||
validKeys++
|
|
||||||
|
|
||||||
// Early skip if we already found two valid keys
|
|
||||||
if validKeys >= 2 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (statDB *StatDB) GetCityStatistics(cityName string) []Weather {
|
|
||||||
statDB.mu.RLock()
|
|
||||||
defer statDB.mu.RUnlock()
|
|
||||||
|
|
||||||
result := make([]Weather, 0)
|
|
||||||
|
|
||||||
for key, record := range statDB.db {
|
|
||||||
if strings.HasSuffix(key, cityName) {
|
|
||||||
result = append(result, record)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package types
|
|
||||||
|
|
||||||
// The WeatherAnomaly data type, representing
|
|
||||||
// skewed meteorological events
|
|
||||||
type WeatherAnomaly struct {
|
|
||||||
Date ZephyrDate `json:"date"`
|
|
||||||
Temp string `json:"temperature"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// The StatResult data type, representing weather statistics
|
|
||||||
// of past meteorological events
|
|
||||||
type StatResult struct {
|
|
||||||
Min string `json:"min"`
|
|
||||||
Max string `json:"max"`
|
|
||||||
Count int `json:"count"`
|
|
||||||
Mean string `json:"mean"`
|
|
||||||
StdDev string `json:"stdDev"`
|
|
||||||
Median string `json:"median"`
|
|
||||||
Mode string `json:"mode"`
|
|
||||||
Anomaly *[]WeatherAnomaly `json:"anomaly"`
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package types
|
|
||||||
|
|
||||||
// Variables type, representing values read from environment variables
|
|
||||||
type Variables struct {
|
|
||||||
Token string
|
|
||||||
TimeToLive int8
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
package types
|
|
||||||
|
|
||||||
// The WeatherAlert data type, representing a
|
|
||||||
// weather alert
|
|
||||||
type WeatherAlert struct {
|
|
||||||
Event string `json:"event"`
|
|
||||||
Start ZephyrAlertDate `json:"startDate"`
|
|
||||||
End ZephyrAlertDate `json:"endDate"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// The Weather data type, representing the weather of a certain location
|
|
||||||
type Weather struct {
|
|
||||||
Date ZephyrDate `json:"date"`
|
|
||||||
Temperature string `json:"temperature"`
|
|
||||||
Min string `json:"min"`
|
|
||||||
Max string `json:"max"`
|
|
||||||
Condition string `json:"condition"`
|
|
||||||
FeelsLike string `json:"feelsLike"`
|
|
||||||
Emoji string `json:"emoji"`
|
|
||||||
Alerts []WeatherAlert `json:"alerts"`
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
package types
|
|
||||||
|
|
||||||
// The Wind data type, representing the wind of a certain location
|
|
||||||
type Wind struct {
|
|
||||||
Arrow string `json:"arrow"`
|
|
||||||
Direction string `json:"direction"`
|
|
||||||
Speed string `json:"speed"`
|
|
||||||
}
|
|
||||||
124
types/zephyr.go
Normal file
124
types/zephyr.go
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// Variables type, representing values read from environment variables
|
||||||
|
type Variables struct {
|
||||||
|
Token string
|
||||||
|
TimeToLive int8
|
||||||
|
}
|
||||||
|
|
||||||
|
// The City data type, representing the name, the latitude and the longitude
|
||||||
|
// of a location
|
||||||
|
type City struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Lat float64 `json:"lat"`
|
||||||
|
Lon float64 `json:"lon"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// The DailyForecastEntity data type, representing the weather forecast
|
||||||
|
// of a single day
|
||||||
|
type DailyForecastEntity struct {
|
||||||
|
Date ZephyrDate `json:"date"`
|
||||||
|
Min string `json:"min"`
|
||||||
|
Max string `json:"max"`
|
||||||
|
Condition string `json:"condition"`
|
||||||
|
Emoji string `json:"emoji"`
|
||||||
|
FeelsLike string `json:"feelsLike"`
|
||||||
|
Wind Wind `json:"wind"`
|
||||||
|
RainProb string `json:"rainProbability"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// The DailyForecast data type, representing a set of DailyForecastEntity
|
||||||
|
type DailyForecast struct {
|
||||||
|
Forecast []DailyForecastEntity `json:"forecast"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// The HourlyForecastEntity data type, representing the weather forecast
|
||||||
|
// of a single hour
|
||||||
|
type HourlyForecastEntity struct {
|
||||||
|
Time ZephyrTime `json:"time"`
|
||||||
|
Temperature string `json:"temperature"`
|
||||||
|
Condition string `json:"condition"`
|
||||||
|
Emoji string `json:"emoji"`
|
||||||
|
Wind Wind `json:"wind"`
|
||||||
|
RainProb string `json:"rainProbability"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// The HourlyForecast data type, representing a set of HourlyForecastEntity
|
||||||
|
type HourlyForecast struct {
|
||||||
|
Forecast []HourlyForecastEntity `json:"forecast"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// The Metrics data type, representing the humidity, pressure and
|
||||||
|
// similar miscellaneous values
|
||||||
|
type Metrics struct {
|
||||||
|
Humidity string `json:"humidity"`
|
||||||
|
Pressure string `json:"pressure"`
|
||||||
|
DewPoint string `json:"dewPoint"`
|
||||||
|
UvIndex string `json:"uvIndex"`
|
||||||
|
Visibility string `json:"visibility"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// The Moon data type, representing the moon phase,
|
||||||
|
// the moon phase icon and the moon progress(%).
|
||||||
|
type Moon struct {
|
||||||
|
Icon string `json:"icon"`
|
||||||
|
Phase string `json:"phase"`
|
||||||
|
Percentage string `json:"percentage"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// The WeatherAnomaly data type, representing
|
||||||
|
// skewed meteorological events
|
||||||
|
type WeatherAnomaly struct {
|
||||||
|
Date ZephyrDate `json:"date"`
|
||||||
|
Temp string `json:"temperature"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// The StateElement data type, representing a statistical record
|
||||||
|
// This type is for internal usage
|
||||||
|
type StatElement struct {
|
||||||
|
Temperature float64
|
||||||
|
Date time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// The StatResult data type, representing weather statistics
|
||||||
|
// of past meteorological events
|
||||||
|
type StatResult struct {
|
||||||
|
Min string `json:"min"`
|
||||||
|
Max string `json:"max"`
|
||||||
|
Count int `json:"count"`
|
||||||
|
Mean string `json:"mean"`
|
||||||
|
StdDev string `json:"stdDev"`
|
||||||
|
Median string `json:"median"`
|
||||||
|
Mode string `json:"mode"`
|
||||||
|
Anomaly *[]WeatherAnomaly `json:"anomaly"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// The WeatherAlert data type, representing a
|
||||||
|
// weather alert
|
||||||
|
type WeatherAlert struct {
|
||||||
|
Event string `json:"event"`
|
||||||
|
Start ZephyrAlertDate `json:"startDate"`
|
||||||
|
End ZephyrAlertDate `json:"endDate"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// The Weather data type, representing the weather of a certain location
|
||||||
|
type Weather struct {
|
||||||
|
Date ZephyrDate `json:"date"`
|
||||||
|
Temperature string `json:"temperature"`
|
||||||
|
Min string `json:"min"`
|
||||||
|
Max string `json:"max"`
|
||||||
|
Condition string `json:"condition"`
|
||||||
|
FeelsLike string `json:"feelsLike"`
|
||||||
|
Emoji string `json:"emoji"`
|
||||||
|
Alerts []WeatherAlert `json:"alerts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// The Wind data type, representing the wind of a certain location
|
||||||
|
type Wind struct {
|
||||||
|
Arrow string `json:"arrow"`
|
||||||
|
Direction string `json:"direction"`
|
||||||
|
Speed string `json:"speed"`
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user