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 - Analyze a single availability target") fmt.Println(" pviz analyze --focus - Focus on a specific time period") fmt.Println(" pviz compare - 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) } }