Initial version -- ~45 minutes of poking around.

This commit is contained in:
Pat Garrity 2026-06-10 13:56:21 -05:00
commit 439c28c1c7
Signed by: pfm
GPG key ID: 0DC16BCA24B270C4
13 changed files with 310 additions and 0 deletions

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
target/
project/target/
project/project/
.version

16
.pre-commit-config.yaml Normal file
View file

@ -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

72
.scalafmt.conf Normal file
View file

@ -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/**"
]
}

3
README.md Normal file
View file

@ -0,0 +1,3 @@
# fizzbuzz
This project is an over-engineered fizzbuzz to buzz your fizz.

59
build.sbt Normal file
View file

@ -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
)
)

1
project/build.properties Normal file
View file

@ -0,0 +1 @@
sbt.version=1.12.11

33
project/plugins.sbt Normal file
View file

@ -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")

View file

@ -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

View file

@ -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
)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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