aboutsummaryrefslogblamecommitdiff
path: root/cmd/pviz/main.go
blob: 89bd78d10d330b5bad9c434ada52d505a9bf1b0e (plain) (tree)
























































































                                                                                                 
                                                  
                 
                                            
         
                              



                                          
                                                          
                         
                                                  
                 
                                      

         
                     
















































                                                                                                    




                                                     






























































                                                                                                         




                                                                                                                       





































                                                                                                              
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)
	}
}