pocketbase/utils/card_calculation.go

323 lines
8.0 KiB
Go

package utils
import (
"encoding/json"
"errors"
"fmt"
"math"
"sort"
"strconv"
)
// Observation struct with json.Number for flexible value handling
type Observation struct {
ObsKey map[string]string `json:"ObsKey"`
ObsValue struct {
Value json.Number `json:"Value"`
} `json:"ObsValue"`
}
type CardCalculationRequest struct {
Formula string `json:"formula"`
RecordCount string `json:"record_count"`
RecodeIndex string `json:"record_index"`
RecordKey string `json:"record_key"`
NumberFormat string `json:"number_format"`
Conversion string `json:"conversion"`
}
func CardCalculation(data []Observation, req CardCalculationRequest) (map[string]string, error) {
// Sort the data in descending order based on TimePeriod
sort.Slice(data, func(i, j int) bool {
return data[i].ObsKey["TIME_PERIOD"] > data[j].ObsKey["TIME_PERIOD"]
})
var selectedData []Observation
if req.RecodeIndex != "" {
// Convert RecodeIndex to an integer
index, err := strconv.Atoi(req.RecodeIndex)
if err != nil || index < 0 || index >= len(data) {
return nil, errors.New("invalid record index")
}
// Select only the requested index record
selectedData = []Observation{data[index]}
} else {
// Determine the number of records to use
n := len(data) // array leangth
if req.RecordCount != "all" {
var err error
n, err = strconv.Atoi(req.RecordCount)
if err != nil || n > len(data) {
return nil, errors.New("invalid record count")
}
}
selectedData = data[:n]
}
var result float64
switch req.Formula {
case "none":
result = none(selectedData)
case "total":
result = sum(selectedData)
case "average":
result = average(selectedData)
case "highest":
result = highest(selectedData)
case "lowest":
result = lowest(selectedData)
case "different":
result = difference(selectedData)
default:
return nil, errors.New("invalid formula")
}
convertedValue := applyConversion(result, req.Conversion, req.NumberFormat)
displayValue := extractRecordKeyValue(selectedData, req.RecordKey, req.Formula, result)
return map[string]string{
"value": convertedValue,
"display_value": displayValue,
}, nil
}
// Helper function to convert json.Number to float64 safely
func getFloatValue(num json.Number) float64 {
val, err := num.Float64()
if err != nil {
fmt.Println("Error converting value:", err)
return 0
}
return val
}
func none(data []Observation) float64 {
total := 0.0
for _, obs := range data {
total += getFloatValue(obs.ObsValue.Value)
}
return total
}
func sum(data []Observation) float64 {
total := 0.0
for _, obs := range data {
total += getFloatValue(obs.ObsValue.Value)
}
return total
}
func average(data []Observation) float64 {
if len(data) == 0 {
return 0
}
return sum(data) / float64(len(data))
}
func highest(data []Observation) float64 {
max := getFloatValue(data[0].ObsValue.Value)
for _, obs := range data {
val := getFloatValue(obs.ObsValue.Value)
if val > max {
max = val
}
}
return max
}
func lowest(data []Observation) float64 {
min := getFloatValue(data[0].ObsValue.Value)
for _, obs := range data {
val := getFloatValue(obs.ObsValue.Value)
if val < min {
min = val
}
}
return min
}
func difference(data []Observation) float64 {
if len(data) < 2 {
return 0
}
latest := getFloatValue(data[0].ObsValue.Value)
previous := getFloatValue(data[1].ObsValue.Value)
if previous == 0 {
return 0
}
return ((latest - previous) / previous) * 100
}
// func applyConversion(value float64, conversion string, numberFormat string) string {
// if numberFormat == "T" {
// value *= 1000000000000
// } else if numberFormat == "B" {
// value *= 1000000000
// } else if numberFormat == "M" {
// value *= 1000000
// } else if numberFormat == "K" {
// value *= 1000
// }
// switch conversion {
// case "T":
// return fmt.Sprintf("%.2fT", value/1000000000000)
// case "B":
// return fmt.Sprintf("%.2fB", value/1000000000)
// case "M":
// return fmt.Sprintf("%.2fM", value/1000000)
// case "K":
// return fmt.Sprintf("%.2fK", value/1000)
// case "%":
// return fmt.Sprintf("%.2f%%", value)
// default:
// return fmt.Sprintf("%.2f", value)
// }
// }
func applyConversion(value float64, conversion string, numberFormat string) string {
if numberFormat == "T" {
value *= 1000000000000
} else if numberFormat == "B" {
value *= 1000000000
} else if numberFormat == "M" {
value *= 1000000
} else if numberFormat == "K" {
value *= 1000
}
// Helper function to format the number properly
formatNumber := func(v float64, suffix string) string {
if v == float64(int(v)) { // Check if v has no decimal part
return fmt.Sprintf("%d%s", int(v), suffix)
}
return fmt.Sprintf("%.2f%s", v, suffix)
}
switch conversion {
case "T":
return formatNumber(value/1000000000000, "T")
case "B":
return formatNumber(value/1000000000, "B")
case "M":
return formatNumber(value/1000000, "M")
case "K":
return formatNumber(value/1000, "K")
case "%":
return formatNumber(value, "%")
default:
return formatNumber(value, "")
}
}
func extractRecordKeyValue(data []Observation, key string, formula string, targetValue float64) string {
if len(data) == 0 {
return ""
}
switch formula {
case "highest", "lowest", "none":
for _, obs := range data {
value, err := obs.ObsValue.Value.Float64()
if err == nil && value == targetValue {
if val, exists := obs.ObsKey[key]; exists {
return val
}
}
}
case "total", "average", "different":
if val1, exists1 := data[0].ObsKey[key]; exists1 {
if val2, exists2 := data[len(data)-1].ObsKey[key]; exists2 {
return val1 + " - " + val2
}
return val1
}
}
return ""
}
//------------------------------------------------------------
// ChartCalculationRequest struct for request parameters
type ChartCalculationRequest struct {
GroupByAndSum string `json:"groupby_and_sum"`
GetTop string `json:"get_top"`
Conversion string `json:"conversion"`
}
// CalculateGroupedSum processes the observations based on the given request
func CalculateGroupedSum(observations []Observation, request ChartCalculationRequest) ([]Observation, error) {
// Map to store the summed values
summedData := make(map[string]float64)
// Iterate through observations and sum values based on GroupBy key
for _, obs := range observations {
groupKey, exists := obs.ObsKey[request.GroupByAndSum]
if !exists {
continue // Skip if the key does not exist in ObsKey
}
value, err := obs.ObsValue.Value.Float64()
if err != nil {
return nil, fmt.Errorf("invalid number format: %v", err)
}
summedData[groupKey] += value
}
// Convert map to slice of Observation
var result []Observation
for key, sum := range summedData {
result = append(result, Observation{
ObsKey: map[string]string{
request.GroupByAndSum: key,
},
ObsValue: struct {
Value json.Number `json:"Value"`
}{
// Value: json.Number(fmt.Sprintf("%.0f", sum)),
Value: json.Number(strconv.FormatFloat(convertValue(sum, request.Conversion), 'f', 6, 64)),
},
})
}
// Sort by summed values in descending order
sort.Slice(result, func(i, j int) bool {
val1, _ := result[i].ObsValue.Value.Float64()
val2, _ := result[j].ObsValue.Value.Float64()
return val1 > val2 // Sort descending
})
// Get the top N values
topN, err := strconv.Atoi(request.GetTop)
if err != nil || topN <= 0 || topN > len(result) {
topN = len(result) // If invalid input, return all
}
return result[:topN], nil
}
// Convert values based on conversion type
func convertValue(value float64, conversion string) float64 {
switch conversion {
case "T": // Trillion
return round(value/1000000000000, 2)
case "B": // Billion
return round(value/1000000000, 2)
case "M": // Million
return round(value/1000000, 2)
case "K": // Thousand
return round(value/1000, 2)
default: // No conversion
return round(value, 2)
}
}
// Function to round a float to 2 decimal places
func round(val float64, precision int) float64 {
p := math.Pow(10, float64(precision))
return math.Round(val*p) / p
}