fizzbuzz.go 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
  1. // Package domain provides the domain logic for the extended FizzBuzz computation.
  2. package domain
  3. import (
  4. "errors"
  5. "fmt"
  6. "math"
  7. "regexp"
  8. "strconv"
  9. )
  10. type (
  11. Empty struct{}
  12. modulo int64
  13. moduloMap map[modulo]struct{}
  14. )
  15. const (
  16. Fizz = "fizz"
  17. Buzz = "buzz"
  18. Both = Fizz + Buzz
  19. )
  20. var (
  21. // RFC3986GenDelims matches the reserved characters in RFC3986 §2.2
  22. //
  23. // See https://www.rfc-editor.org/rfc/rfc3986#section-2.2
  24. RFC3986GenDelims = regexp.MustCompile(`[:|/\?#\[\]@]`)
  25. Void Empty
  26. )
  27. // IsValid returns true if the received is in the open interval ]1,limit[,
  28. // and is not already present in the existing map.
  29. func (m modulo) IsValid(limit modulo, existing moduloMap) bool {
  30. if m <= 1 || m > limit {
  31. return false
  32. }
  33. if _, found := existing[m]; found {
  34. return false
  35. }
  36. return true
  37. }
  38. // FizzBuzzes is an ordered list of FizzBuzz strings;
  39. type FizzBuzzes []string
  40. // validateNumbers fails if either of the numbers fails validation.
  41. //
  42. // A successful return means that limit is >= 1 and int1 and int2 are different and within [1,limit[.
  43. func validateNumbers(int1, int2, limit int) error {
  44. existing := make(moduloMap, 2)
  45. l := modulo(limit)
  46. if !l.IsValid(math.MaxInt, existing) {
  47. return errors.New("limit is invalid")
  48. }
  49. m1 := modulo(int1)
  50. if !m1.IsValid(l, existing) {
  51. return errors.New("int1 is invalid")
  52. }
  53. existing[m1] = Void
  54. m2 := modulo(int2)
  55. if !m2.IsValid(l, existing) {
  56. return errors.New("int2 is invalid")
  57. }
  58. return nil
  59. }
  60. func validateStrings(str1, str2 string) error {
  61. validateString := func(s string) error {
  62. if len(s) == 0 {
  63. return fmt.Errorf("invalid string parameter: empty")
  64. }
  65. if RFC3986GenDelims.MatchString(s) {
  66. return fmt.Errorf("contains a RFC-3986 reserved character")
  67. }
  68. return nil
  69. }
  70. ss := [...]string{str1, str2}
  71. for i, s := range ss {
  72. if err := validateString(s); err != nil {
  73. return fmt.Errorf("validateString(%d): %w", i, err)
  74. }
  75. }
  76. return nil
  77. }
  78. // FizzBuzz builds a list of strings with numbers in the closed interval [1,limit], where:
  79. //
  80. // - all multiples of int1 are replaced by str1, except those that are also multiples of int2
  81. // - all multiples of int2 are replaced by str2, except those that are also multiples of int1
  82. // - all multiples of int1 and int2 are replaced by str1str2.
  83. func FizzBuzz(int1, int2, limit int, str1, str2 string) (FizzBuzzes, error) {
  84. if err := validateNumbers(int1, int2, limit); err != nil {
  85. return nil, fmt.Errorf("invalid modulo parameters: %w", err)
  86. }
  87. if err := validateStrings(str1, str2); err != nil {
  88. return nil, fmt.Errorf("invalid string parameters: %w", err)
  89. }
  90. both := str1 + str2
  91. fbs := make(FizzBuzzes, 0, limit)
  92. // Arithmetic for loop on append to a preallocated slice is usually faster than
  93. // indexed access as in fbs := make(FizzBuzzes, limit); loop on fbs[i] = <value>.
  94. for i := 1; i <= limit; i++ {
  95. // The switch is more readable than the usual nested "if" solution.
  96. switch {
  97. case i%int1 == 0 && i%int2 == 0:
  98. fbs = append(fbs, both)
  99. case i%int1 == 0:
  100. fbs = append(fbs, str1)
  101. case i%int2 == 0:
  102. fbs = append(fbs, str2)
  103. default:
  104. fbs = append(fbs, strconv.Itoa(i))
  105. }
  106. }
  107. return fbs, nil
  108. }