123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119 |
- // 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] = <value>.
- 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
- }
|