package main
import (
"fmt"
"os"
"strconv"
"text/tabwriter"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
type Period string
const (
PeriodDay Period = "day"
PeriodWeek Period = "week"
PeriodMonth Period = "month"
PeriodQuarter Period = "quarter"
PeriodYear Period = "year"
)
var periodMinutes = map[Period]float64{
PeriodDay: 24 * 60,
PeriodWeek: 7 * 24 * 60,
PeriodMonth: 30 * 24 * 60,
PeriodQuarter: 90 * 24 * 60,
PeriodYear: 365 * 24 * 60,
}
var periodOrder = []Period{PeriodDay, PeriodWeek, PeriodMonth, PeriodQuarter, PeriodYear}
type TimeUnit struct {
Minutes float64
Formatted string
}
type AvailabilityCalculator struct {
availability float64
}
func NewAvailabilityCalculator(availabilityPercentage float64) (*AvailabilityCalculator, error) {
if availabilityPercentage < 0 || availabilityPercentage > 100 {
return nil, fmt.Errorf("availability must be between 0 and 100")
}
return &AvailabilityCalculator{
availability: availabilityPercentage / 100.0,
}, nil
}
func (ac *AvailabilityCalculator) CalculateDowntime(period Period) TimeUnit {
totalMinutes := periodMinutes[period]
downtimeMinutes := totalMinutes * (1 - ac.availability)
return TimeUnit{
Minutes: downtimeMinutes,
Formatted: formatDuration(downtimeMinutes),
}
}
func (ac *AvailabilityCalculator) CalculateAllPeriods() map[Period]TimeUnit {
result := make(map[Period]TimeUnit)
for _, period := range periodOrder {
result[period] = ac.CalculateDowntime(period)
}
return result
}
func formatDuration(minutes float64) string {
if minutes < 1 {
return fmt.Sprintf("%.1f seconds", minutes*60)
}
hours := int(minutes / 60)
remainingMinutes := minutes - float64(hours*60)
if hours == 0 {
return fmt.Sprintf("%.1f minutes", remainingMinutes)
} else if remainingMinutes == 0 {
return fmt.Sprintf("%d hours", hours)
} else {
return fmt.Sprintf("%d hours %.1f minutes", hours, remainingMinutes)
}
}
func printTable(headers []string, rows [][]string) {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
for i, header := range headers {
if i > 0 {
fmt.Fprint(w, "\t")
}
fmt.Fprint(w, header)
}
fmt.Fprintln(w)
for _, row := range rows {
for i, cell := range row {
if i > 0 {
fmt.Fprint(w, "\t")
}
fmt.Fprint(w, cell)
}
fmt.Fprintln(w)
}
w.Flush()
}
func analyzeSingle(availability float64, focusPeriod string) error {
calc, err := NewAvailabilityCalculator(availability)
if err != nil {
return err
}
var periods []Period
if focusPeriod != "" {
periods = []Period{Period(focusPeriod)}
} else {
periods = periodOrder
}
headers := []string{"Time Period", "Maximum Downtime"}
var rows [][]string
caser := cases.Title(language.English)
for _, period := range periods {
downtime := calc.CalculateDowntime(period)
rows = append(rows, []string{
caser.String(string(period)),
downtime.Formatted,
})
}
printTable(headers, rows)
return nil
}
func compareAvailabilities(availability1, availability2 float64) error {
if availability1 == availability2 {
return fmt.Errorf("cannot compare identical availability values")
}
calc1, err := NewAvailabilityCalculator(availability1)
if err != nil {
return err
}
calc2, err := NewAvailabilityCalculator(availability2)
if err != nil {
return err
}
improvement := ((100 - availability1) - (100 - availability2)) / (100 - availability1) * 100
fmt.Printf("Downtime reduction: %.1f%%\n\n", improvement)
headers := []string{"Time Period", fmt.Sprintf("%.2f%%", availability1), fmt.Sprintf("%.2f%%", availability2)}
var rows [][]string
caser := cases.Title(language.English)
for _, period := range periodOrder {
downtime1 := calc1.CalculateDowntime(period)
downtime2 := calc2.CalculateDowntime(period)
rows = append(rows, []string{
caser.String(string(period)),
downtime1.Formatted,
downtime2.Formatted,
})
}
printTable(headers, rows)
return nil
}
func printUsage() {
fmt.Println("Usage:")
fmt.Println(" pviz analyze <availability> - Analyze a single availability target")
fmt.Println(" pviz analyze <availability> --focus <period> - Focus on a specific time period")
fmt.Println(" pviz compare <availability1> <availability2> - Compare two availability targets")
fmt.Println()
fmt.Println("Examples:")
fmt.Println(" pviz analyze 99.99")
fmt.Println(" pviz analyze 99.99 --focus quarter")
fmt.Println(" pviz compare 99.9 99.95")
fmt.Println()
fmt.Println("Valid periods: day, week, month, quarter, year")
}
func main() {
if len(os.Args) < 2 {
printUsage()
os.Exit(1)
}
command := os.Args[1]
switch command {
case "analyze":
if len(os.Args) < 3 {
fmt.Fprintf(os.Stderr, "Error: analyze command requires availability argument\n")
os.Exit(1)
}
availability, err := strconv.ParseFloat(os.Args[2], 64)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: invalid availability value: %v\n", err)
os.Exit(1)
}
var focusPeriod string
if len(os.Args) >= 5 && os.Args[3] == "--focus" {
focusPeriod = os.Args[4]
validPeriod := false
for _, p := range periodOrder {
if string(p) == focusPeriod {
validPeriod = true
break
}
}
if !validPeriod {
fmt.Fprintf(os.Stderr, "Error: invalid period '%s'. Valid periods: day, week, month, quarter, year\n", focusPeriod)
os.Exit(1)
}
}
if err := analyzeSingle(availability, focusPeriod); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
case "compare":
if len(os.Args) < 4 {
fmt.Fprintf(os.Stderr, "Error: compare command requires two availability arguments\n")
os.Exit(1)
}
availability1, err := strconv.ParseFloat(os.Args[2], 64)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: invalid first availability value: %v\n", err)
os.Exit(1)
}
availability2, err := strconv.ParseFloat(os.Args[3], 64)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: invalid second availability value: %v\n", err)
os.Exit(1)
}
if err := compareAvailabilities(availability1, availability2); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
default:
fmt.Fprintf(os.Stderr, "Error: unknown command '%s'\n", command)
printUsage()
os.Exit(1)
}
}