package powerball import ( "context" "fmt" "io" "strconv" "strings" "time" "gitea.stevedudenhoeffer.com/steve/go-extractor" "golang.org/x/text/currency" ) type Config struct { } var DefaultConfig = Config{} func (c Config) validate() Config { return c } type Drawing struct { Date time.Time Numbers [5]int PowerBall int PowerPlay int } type NextDrawing struct { Date string Jackpot currency.Amount } func deferClose(cl io.Closer) { if cl != nil { _ = cl.Close() } } func getDrawing(_ context.Context, doc extractor.Document) (*Drawing, error) { var drawing Drawing nums := doc.Select("div.game-ball-group div.white-balls") if len(nums) != 5 { return nil, fmt.Errorf("expected 5 white balls, got %d", len(nums)) } for i, num := range nums { txt, err := num.Text() if err != nil { return nil, fmt.Errorf("failed to get white ball %d: %w", i, err) } val, err := strconv.Atoi(txt) if err != nil { return nil, fmt.Errorf("failed to convert white ball %d to int: %w", i, err) } drawing.Numbers[i] = val } powerball := doc.Select("div.game-ball-group div.powerball") if len(powerball) != 1 { return nil, fmt.Errorf("expected 1 powerball, got %d", len(powerball)) } txt, err := powerball[0].Text() if err != nil { return nil, fmt.Errorf("failed to get powerball: %w", err) } val, err := strconv.Atoi(txt) if err != nil { return nil, fmt.Errorf("failed to convert powerball to int: %w", err) } drawing.PowerBall = val powerplay := doc.Select("span.power-play span.multiplier") if len(powerplay) != 1 { return nil, fmt.Errorf("expected 1 powerplay, got %d", len(powerplay)) } // powerplay is in the format of "2X" or "3X" etc. txt, err = powerplay[0].Text() if err != nil { return nil, fmt.Errorf("failed to get powerplay: %w", err) } val, err = strconv.Atoi(strings.ReplaceAll(strings.ReplaceAll(txt, "X", ""), "x", "")) if err != nil { return nil, fmt.Errorf("failed to convert powerplay to int: %w", err) } drawing.PowerPlay = val return &drawing, nil } func getNextDrawing(_ context.Context, doc extractor.Document) (*NextDrawing, error) { var nextDrawing NextDrawing date := doc.Select("div.next-powerball h5.title-date") if len(date) != 1 { return nil, fmt.Errorf("expected 1 date, got %d", len(date)) } var err error nextDrawing.Date, err = date[0].Text() if err != nil { return nil, fmt.Errorf("failed to get date: %w", err) } jackpot := doc.Select("div.next-powerball div.game-detail-group span.game-jackpot-number") if len(jackpot) != 1 { return nil, fmt.Errorf("expected 1 jackpot, got %d", len(jackpot)) } txt, err := jackpot[0].Text() if err != nil { return nil, fmt.Errorf("failed to get jackpot: %w", err) } // jackpot is in the format of "$1.5 billion", "$100 million", or "$200,000" etc // make one filter to only get the numeric part of the jackpot numericOnly := func(in string) float64 { var out string for _, r := range in { if r >= '0' && r <= '9' { out += string(r) } if r == '.' { out += string(r) } } val, err := strconv.ParseFloat(out, 64) if err != nil { return 0 } return val } numeric := numericOnly(txt) set := false if strings.Contains(txt, "Billion") { amt := currency.USD.Amount(numeric * 1000000000) nextDrawing.Jackpot = amt set = true } else if strings.Contains(txt, "Million") { amt := currency.USD.Amount(numeric * 1000000) nextDrawing.Jackpot = amt set = true } else { amt := currency.USD.Amount(numeric) nextDrawing.Jackpot = amt set = true } if !set { return nil, fmt.Errorf("failed to convert jackpot to currency: %w", err) } return &nextDrawing, nil } func (c Config) GetCurrent(ctx context.Context, b extractor.Browser) (*Drawing, *NextDrawing, error) { c = c.validate() doc, err := b.Open(ctx, "https://www.powerball.com/", extractor.OpenPageOptions{}) if err != nil { return nil, nil, err } defer deferClose(doc) d, err := getDrawing(ctx, doc) if err != nil { return nil, nil, err } nd, err := getNextDrawing(ctx, doc) if err != nil { return nil, nil, err } return d, nd, nil } func GetCurrent(ctx context.Context, b extractor.Browser) (*Drawing, *NextDrawing, error) { return DefaultConfig.GetCurrent(ctx, b) }