Initial commit
- REST API for calculating age breakdowns (years, months, weeks, days, hours, minutes, seconds)
- Birthtime configured as Unix timestamps
- Sleeps until next birthday countdown
- Per-person lookup via GET /age/{name}
- Docker and Makefile build support
- Woodpecker CI pipelines
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
People []Person `yaml:"people"`
|
||||
}
|
||||
|
||||
type Person struct {
|
||||
Name string `yaml:"name"`
|
||||
Birthtime int64 `yaml:"birthtime"`
|
||||
}
|
||||
|
||||
type AgeBreakdown struct {
|
||||
Years int `json:"years"`
|
||||
Months int `json:"months"`
|
||||
Weeks int `json:"weeks"`
|
||||
Days int `json:"days"`
|
||||
Hours int64 `json:"hours"`
|
||||
Minutes int64 `json:"minutes"`
|
||||
Seconds int64 `json:"seconds"`
|
||||
}
|
||||
|
||||
type PersonAge struct {
|
||||
Name string `json:"name"`
|
||||
DOB string `json:"dob"`
|
||||
Birthtime int64 `json:"birthtime"`
|
||||
SleepsUntilBirthday int `json:"sleeps_until_birthday"`
|
||||
Age AgeBreakdown `json:"age"`
|
||||
}
|
||||
|
||||
func sleepsUntilBirthday(dob time.Time, now time.Time) int {
|
||||
next := time.Date(now.Year(), dob.Month(), dob.Day(), 0, 0, 0, 0, now.Location())
|
||||
if !next.After(now) {
|
||||
next = time.Date(now.Year()+1, dob.Month(), dob.Day(), 0, 0, 0, 0, now.Location())
|
||||
}
|
||||
return int(next.Sub(now).Hours()/24) + 1
|
||||
}
|
||||
|
||||
func calculateAge(dob time.Time, now time.Time) AgeBreakdown {
|
||||
// Calendar-based years and months
|
||||
years := now.Year() - dob.Year()
|
||||
months := int(now.Month()) - int(dob.Month())
|
||||
if now.Day() < dob.Day() {
|
||||
months--
|
||||
}
|
||||
if months < 0 {
|
||||
years--
|
||||
months += 12
|
||||
}
|
||||
|
||||
totalMonths := years*12 + months
|
||||
|
||||
// Total duration for weeks/days/hours/minutes/seconds
|
||||
dur := now.Sub(dob)
|
||||
totalDays := int(dur.Hours() / 24)
|
||||
totalWeeks := totalDays / 7
|
||||
|
||||
return AgeBreakdown{
|
||||
Years: years,
|
||||
Months: totalMonths,
|
||||
Weeks: totalWeeks,
|
||||
Days: totalDays,
|
||||
Hours: int64(dur.Hours()),
|
||||
Minutes: int64(dur.Minutes()),
|
||||
Seconds: int64(dur.Seconds()),
|
||||
}
|
||||
}
|
||||
|
||||
func loadConfig(path string) (*Config, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading config: %w", err)
|
||||
}
|
||||
var cfg Config
|
||||
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
||||
return nil, fmt.Errorf("parsing config: %w", err)
|
||||
}
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
configPath := "config.yaml"
|
||||
if v := os.Getenv("CONFIG_PATH"); v != "" {
|
||||
configPath = v
|
||||
}
|
||||
|
||||
cfg, err := loadConfig(configPath)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to load config: %v", err)
|
||||
}
|
||||
|
||||
// Index people by lowercase name
|
||||
people := make(map[string]Person)
|
||||
for _, p := range cfg.People {
|
||||
people[strings.ToLower(p.Name)] = p
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// GET /age - all people
|
||||
mux.HandleFunc("GET /age", func(w http.ResponseWriter, r *http.Request) {
|
||||
now := time.Now()
|
||||
var results []PersonAge
|
||||
for _, p := range cfg.People {
|
||||
dob := time.Unix(p.Birthtime, 0)
|
||||
results = append(results, PersonAge{
|
||||
Name: p.Name,
|
||||
DOB: dob.Format("2006-01-02"),
|
||||
Birthtime: p.Birthtime,
|
||||
SleepsUntilBirthday: sleepsUntilBirthday(dob, now),
|
||||
Age: calculateAge(dob, now),
|
||||
})
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(results)
|
||||
})
|
||||
|
||||
// GET /age/{name} - single person
|
||||
mux.HandleFunc("GET /age/{name}", func(w http.ResponseWriter, r *http.Request) {
|
||||
name := strings.ToLower(r.PathValue("name"))
|
||||
p, ok := people[name]
|
||||
if !ok {
|
||||
http.Error(w, fmt.Sprintf("person %q not found", name), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
dob := time.Unix(p.Birthtime, 0)
|
||||
result := PersonAge{
|
||||
Name: p.Name,
|
||||
DOB: dob.Format("2006-01-02"),
|
||||
Birthtime: p.Birthtime,
|
||||
SleepsUntilBirthday: sleepsUntilBirthday(dob, now),
|
||||
Age: calculateAge(dob, now),
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(result)
|
||||
})
|
||||
|
||||
addr := ":8080"
|
||||
if v := os.Getenv("LISTEN_ADDR"); v != "" {
|
||||
addr = v
|
||||
}
|
||||
log.Printf("Listening on %s", addr)
|
||||
log.Fatal(http.ListenAndServe(addr, mux))
|
||||
}
|
||||
Reference in New Issue
Block a user