// Package domain provides the domain logic for the extended FizzBuzz computation. package domain import ( "errors" "fmt" "math" "regexp" "strconv" ) type ( Empty struct{} modulo int64 moduloMap map[modulo]struct{} ) const ( Fizz = "fizz" Buzz = "buzz" Both = Fizz + Buzz ) var ( // RFC3986GenDelims matches the reserved characters in RFC3986 ยง2.2 // // See https://www.rfc-editor.org/rfc/rfc3986#section-2.2 RFC3986GenDelims = regexp.MustCompile(`[:|/\?#\[\]@]`) Void Empty ) // IsValid returns true if the received is in the open interval ]1,limit[, // and is not already present in the existing map. func (m modulo) IsValid(limit modulo, existing moduloMap) bool { if m <= 1 || m > limit { return false } if _, found := existing[m]; found { return false } return true } // FizzBuzzes is an ordered list of FizzBuzz strings; type FizzBuzzes []string // validateNumbers fails if either of the numbers fails validation. // // A successful return means that limit is >= 1 and int1 and int2 are different and within [1,limit[. func validateNumbers(int1, int2, limit int) error { existing := make(moduloMap, 2) l := modulo(limit) if !l.IsValid(math.MaxInt, existing) { return errors.New("limit is invalid") } m1 := modulo(int1) if !m1.IsValid(l, existing) { return errors.New("int1 is invalid") } existing[m1] = Void m2 := modulo(int2) if !m2.IsValid(l, existing) { return errors.New("int2 is invalid") } return nil } func validateStrings(str1, str2 string) error { validateString := func(s string) error { if len(s) == 0 { return fmt.Errorf("invalid string parameter: empty") } if RFC3986GenDelims.MatchString(s) { return fmt.Errorf("contains a RFC-3986 reserved character") } return nil } ss := [...]string{str1, str2} for i, s := range ss { if err := validateString(s); err != nil { return fmt.Errorf("validateString(%d): %w", i, err) } } return nil } // FizzBuzz builds a list of strings with numbers in the closed interval [1,limit], where: // // - all multiples of int1 are replaced by str1, except those that are also multiples of int2 // - all multiples of int2 are replaced by str2, except those that are also multiples of int1 // - all multiples of int1 and int2 are replaced by str1str2. func FizzBuzz(int1, int2, limit int, str1, str2 string) (FizzBuzzes, error) { if err := validateNumbers(int1, int2, limit); err != nil { return nil, fmt.Errorf("invalid modulo parameters: %w", err) } if err := validateStrings(str1, str2); err != nil { return nil, fmt.Errorf("invalid string parameters: %w", err) } both := str1 + str2 fbs := make(FizzBuzzes, 0, limit) // Arithmetic for loop on append to a preallocated slice is usually faster than // indexed access as in fbs := make(FizzBuzzes, limit); loop on fbs[i] = . for i := 1; i <= limit; i++ { // The switch is more readable than the usual nested "if" solution. switch { case i%int1 == 0 && i%int2 == 0: fbs = append(fbs, both) case i%int1 == 0: fbs = append(fbs, str1) case i%int2 == 0: fbs = append(fbs, str2) default: fbs = append(fbs, strconv.Itoa(i)) } } return fbs, nil }