From 439c28c1c767304ee32afdcbfa67ea8a556fb69c Mon Sep 17 00:00:00 2001 From: Pat Garrity Date: Wed, 10 Jun 2026 13:56:21 -0500 Subject: [PATCH] Initial version -- ~45 minutes of poking around. --- .gitignore | 4 ++ .pre-commit-config.yaml | 16 +++++ .scalafmt.conf | 72 +++++++++++++++++++ README.md | 3 + build.sbt | 59 +++++++++++++++ project/build.properties | 1 + project/plugins.sbt | 33 +++++++++ src/main/scala/gs/fizzbuzz/Condition.scala | 18 +++++ .../scala/gs/fizzbuzz/DefaultCondition.scala | 15 ++++ .../scala/gs/fizzbuzz/DefaultFizzBuzz.scala | 26 +++++++ src/main/scala/gs/fizzbuzz/FizzBuzz.scala | 8 +++ src/main/scala/gs/fizzbuzz/Main.scala | 42 +++++++++++ src/main/scala/gs/fizzbuzz/Result.scala | 13 ++++ 13 files changed, 310 insertions(+) create mode 100644 .gitignore create mode 100644 .pre-commit-config.yaml create mode 100644 .scalafmt.conf create mode 100644 README.md create mode 100644 build.sbt create mode 100644 project/build.properties create mode 100644 project/plugins.sbt create mode 100644 src/main/scala/gs/fizzbuzz/Condition.scala create mode 100644 src/main/scala/gs/fizzbuzz/DefaultCondition.scala create mode 100644 src/main/scala/gs/fizzbuzz/DefaultFizzBuzz.scala create mode 100644 src/main/scala/gs/fizzbuzz/FizzBuzz.scala create mode 100644 src/main/scala/gs/fizzbuzz/Main.scala create mode 100644 src/main/scala/gs/fizzbuzz/Result.scala diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0e79824 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +target/ +project/target/ +project/project/ +.version diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..fbcf79c --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,16 @@ +--- +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: end-of-file-fixer + - id: trailing-whitespace + - id: fix-byte-order-marker + - id: mixed-line-ending + args: ['--fix=lf'] + description: Enforces using only 'LF' line endings. + - id: trailing-whitespace + - repo: https://git.garrity.co/garrity-software/gs-pre-commit-scala + rev: v1.0.1 + hooks: + - id: scalafmt diff --git a/.scalafmt.conf b/.scalafmt.conf new file mode 100644 index 0000000..9c7929b --- /dev/null +++ b/.scalafmt.conf @@ -0,0 +1,72 @@ +// See: https://github.com/scalameta/scalafmt/tags for the latest tags. +version = 3.8.1 +runner.dialect = scala3 +maxColumn = 80 + +rewrite { + rules = [RedundantBraces, RedundantParens, Imports, SortModifiers] + imports.expand = true + imports.sort = scalastyle + redundantBraces.ifElseExpressions = true + redundantBraces.stringInterpolation = true +} + +indent { + main = 2 + callSite = 2 + defnSite = 2 + extendSite = 4 + withSiteRelativeToExtends = 2 + commaSiteRelativeToExtends = 2 +} + +align { + preset = more + openParenCallSite = false + openParenDefnSite = false +} + +newlines { + implicitParamListModifierForce = [before,after] + topLevelStatementBlankLines = [ + { + blanks = 1 + } + ] + afterCurlyLambdaParams = squash +} + +danglingParentheses { + defnSite = true + callSite = true + ctrlSite = true + exclude = [] +} + +verticalMultiline { + atDefnSite = true + arityThreshold = 2 + newlineAfterOpenParen = true +} + +comments { + wrap = standalone +} + +docstrings { + style = "SpaceAsterisk" + oneline = unfold + wrap = yes + forceBlankLineBefore = true +} + +project { + excludePaths = [ + "glob:**target/**", + "glob:**.metals/**", + "glob:**.bloop/**", + "glob:**.bsp/**", + "glob:**metals.sbt", + "glob:**.git/**" + ] +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..420db23 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# fizzbuzz + +This project is an over-engineered fizzbuzz to buzz your fizz. diff --git a/build.sbt b/build.sbt new file mode 100644 index 0000000..f011fc8 --- /dev/null +++ b/build.sbt @@ -0,0 +1,59 @@ +val scala3: String = "3.8.4" + +ThisBuild / scalaVersion := scala3 +ThisBuild / gsProjectName := "fizzbuzz" + +ThisBuild / externalResolvers := Seq( + "Garrity Software Mirror" at "https://maven.garrity.co/releases", + "Garrity Software Releases" at "https://maven.garrity.co/gs" +) + +lazy val sharedSettings = Seq( + scalaVersion := scala3, + version := calVer.value, + publish / skip := true, + publishLocal / skip := true, + publishArtifact := false +) + +val Deps = new { + val Cats = new { + val Core: ModuleID = "org.typelevel" %% "cats-core" % "2.13.0" + val Effect: ModuleID = "org.typelevel" %% "cats-effect" % "3.7.0" + } + + val Fs2 = new { + private val Version: String = "3.13.0" + + val Core: ModuleID = "co.fs2" %% "fs2-core" % Version + val IO: ModuleID = "co.fs2" %% "fs2-io" % Version + } + + val Gs = new { + val Std: ModuleID = "gs" %% "gs-std-core-v0" % "0.1.3" + val Datagen: ModuleID = "gs" %% "gs-datagen-core-v0" % "0.4.1" + } + + val MUnit: ModuleID = "org.scalameta" %% "munit" % "1.3.0" +} + +lazy val testSettings = Seq( + libraryDependencies ++= Seq( + Deps.MUnit % Test, + Deps.Gs.Datagen % Test + ) +) + +lazy val fizzbuzz = project + .in(file(".")) + .settings(sharedSettings) + .settings(testSettings) + .settings(name := s"${gsProjectName.value}") + .settings(fork := true) + .settings( + libraryDependencies ++= Seq( + Deps.Cats.Core, + Deps.Cats.Effect, + Deps.Fs2.Core + ) + ) diff --git a/project/build.properties b/project/build.properties new file mode 100644 index 0000000..dabdb15 --- /dev/null +++ b/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.12.11 diff --git a/project/plugins.sbt b/project/plugins.sbt new file mode 100644 index 0000000..18a6411 --- /dev/null +++ b/project/plugins.sbt @@ -0,0 +1,33 @@ +def selectCredentials(): Credentials = + if ((Path.userHome / ".sbt" / ".credentials").exists()) + Credentials(Path.userHome / ".sbt" / ".credentials") + else + Credentials.apply( + realm = "Reposilite", + host = "maven.garrity.co", + userName = sys.env + .get("GS_MAVEN_USER") + .getOrElse( + throw new RuntimeException( + "You must either provide ~/.sbt/.credentials or specify the GS_MAVEN_USER environment variable." + ) + ), + passwd = sys.env + .get("GS_MAVEN_TOKEN") + .getOrElse( + throw new RuntimeException( + "You must either provide ~/.sbt/.credentials or specify the GS_MAVEN_TOKEN environment variable." + ) + ) + ) + +credentials += selectCredentials() + +externalResolvers := Seq( + "Garrity Software Mirror" at "https://maven.garrity.co/releases", + "Garrity Software Releases" at "https://maven.garrity.co/gs" +) + +addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.4.4") +addSbtPlugin("gs" % "sbt-garrity-software" % "0.7.0") +addSbtPlugin("gs" % "sbt-gs-calver" % "0.2.0") diff --git a/src/main/scala/gs/fizzbuzz/Condition.scala b/src/main/scala/gs/fizzbuzz/Condition.scala new file mode 100644 index 0000000..312aade --- /dev/null +++ b/src/main/scala/gs/fizzbuzz/Condition.scala @@ -0,0 +1,18 @@ +package gs.fizzbuzz + +import cats.Applicative + +trait Condition[F[_], Input, Output]: + def matches(input: Input): F[Boolean] + def output: Output +end Condition + +object Condition: + + final class Noop[F[_]: Applicative, Input] extends Condition[F, Input, Nothing]: + override def matches(input: Input): F[Boolean] = + Applicative[F].pure(false) + override def output: Nothing = + throw new IllegalStateException("No-op condition cannot produce output.") + +end Condition diff --git a/src/main/scala/gs/fizzbuzz/DefaultCondition.scala b/src/main/scala/gs/fizzbuzz/DefaultCondition.scala new file mode 100644 index 0000000..ee50a14 --- /dev/null +++ b/src/main/scala/gs/fizzbuzz/DefaultCondition.scala @@ -0,0 +1,15 @@ +package gs.fizzbuzz + +import cats.Applicative +import cats.Monoid + +final class DefaultCondition[F[_]: Applicative, Input, Out]( + val comparison: Input, + val output: Out, + val operator: Monoid[Input] +)(using CanEqual[Input, Input]) extends Condition[F, Input, Out]: + + override def matches(input: Input): F[Boolean] = + Applicative[F].pure( + operator.combine(input, comparison) == operator.empty + ) diff --git a/src/main/scala/gs/fizzbuzz/DefaultFizzBuzz.scala b/src/main/scala/gs/fizzbuzz/DefaultFizzBuzz.scala new file mode 100644 index 0000000..6433728 --- /dev/null +++ b/src/main/scala/gs/fizzbuzz/DefaultFizzBuzz.scala @@ -0,0 +1,26 @@ +package gs.fizzbuzz + +import cats.syntax.all.* +import fs2.Pipe +import cats.FlatMap + +class DefaultFizzBuzz[F[_]: FlatMap, Input, Out]( + val fizzCondition: Condition[F, Input, Out], + val buzzCondition: Condition[F, Input, Out] +) extends FizzBuzz[F, Input, Out]: + def evaluate(input: Input): F[Result[Out]] = + for + fizz <- fizzCondition.matches(input) + buzz <- buzzCondition.matches(input) + yield + if fizz && buzz then + Result.FizzBuzz(fizzCondition.output, buzzCondition.output) + else if fizz then + Result.Fizz(fizzCondition.output) + else if buzz then + Result.Buzz(buzzCondition.output) + else + Result.Unmatched + + def pipe(): Pipe[F, Input, Result[Out]] = in => in.evalMap(evaluate) +end DefaultFizzBuzz diff --git a/src/main/scala/gs/fizzbuzz/FizzBuzz.scala b/src/main/scala/gs/fizzbuzz/FizzBuzz.scala new file mode 100644 index 0000000..77333ee --- /dev/null +++ b/src/main/scala/gs/fizzbuzz/FizzBuzz.scala @@ -0,0 +1,8 @@ +package gs.fizzbuzz + +import fs2.Pipe + +trait FizzBuzz[F[_], Input, Out]: + def evaluate(input: Input): F[Result[Out]] + def pipe(): Pipe[F, Input, Result[Out]] +end FizzBuzz diff --git a/src/main/scala/gs/fizzbuzz/Main.scala b/src/main/scala/gs/fizzbuzz/Main.scala new file mode 100644 index 0000000..256dd70 --- /dev/null +++ b/src/main/scala/gs/fizzbuzz/Main.scala @@ -0,0 +1,42 @@ +package gs.fizzbuzz + +import cats.Monoid +import cats.effect.IO +import cats.effect.IOApp +import cats.effect.ExitCode + +object Main extends IOApp: + override def run(args: List[String]): IO[ExitCode] = + val op: Monoid[Long] = new Monoid[Long] { + override def empty: Long = 0L + override def combine(x: Long, y: Long): Long = x % y + } + + val fizzCond: Condition[IO, Long, String] = new DefaultCondition( + comparison = 3L, + output = "Fizz", + operator = op + ) + + val buzzCond: Condition[IO, Long, String] = new DefaultCondition( + comparison = 5L, + output = "Buzz", + operator = op + ) + + val fb: FizzBuzz[IO, Long, String] = new DefaultFizzBuzz[IO, Long, String]( + fizzCondition = fizzCond, + buzzCondition = buzzCond + ) + + for + ex1 <- fb.evaluate(15) + ex2 <- fb.evaluate(9) + ex3 <- fb.evaluate(20) + ex4 <- fb.evaluate(13) + _ <- IO.println(ex1) + _ <- IO.println(ex2) + _ <- IO.println(ex3) + _ <- IO.println(ex4) + yield + ExitCode.Success diff --git a/src/main/scala/gs/fizzbuzz/Result.scala b/src/main/scala/gs/fizzbuzz/Result.scala new file mode 100644 index 0000000..e5e5d5a --- /dev/null +++ b/src/main/scala/gs/fizzbuzz/Result.scala @@ -0,0 +1,13 @@ +package gs.fizzbuzz + +sealed trait Result[+Out] + +object Result: + + case class Fizz[Out](fizz: Out) extends Result[Out] + case class Buzz[Out](buzz: Out) extends Result[Out] + case class FizzBuzz[Out](fizz: Out, buzz: Out) extends Result[Out] + case object Unmatched extends Result[Nothing]: + override def toString(): String = "unmatched" + +end Result