Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save chaotic3quilibrium/51744cecc2d2202e4db97b972509f8df to your computer and use it in GitHub Desktop.
Save chaotic3quilibrium/51744cecc2d2202e4db97b972509f8df to your computer and use it in GitHub Desktop.
Java Code Challenge: Scrabble Sets - Scala Solution
package org.public_domain.scrabble
import scala.util.{Success, Failure, Random, Try}
object Bag {
//copy and paste directly from provided URL at "Tile count and value ordered by count"
// with "Blank" replaced with "_":
// http://scrabblewizard.com/scrabble-tile-distribution/
private val countByTileFull =
"""E 12 1
|A 9 1
|I 9 1
|O 8 1
|N 6 1
|R 6 1
|T 6 1
|L 4 1
|S 4 1
|U 4 1
|D 4 2
|G 3 2
|_ 2 0
|B 2 3
|C 2 3
|M 2 3
|P 2 3
|F 2 4
|H 2 4
|V 2 4
|W 2 4
|Y 2 4
|K 1 5
|J 1 8
|X 1 8
|Q 1 10
|Z 1 10
|"""
.stripMargin
.lines
.filter(_.nonEmpty)
.map(
line => {
val char =
line.head.toUpper
val count =
line.drop(2).takeWhile(_.isDigit).toInt
val value =
line.drop(2).dropWhile(_.isDigit).tail.takeWhile(_.isDigit).toInt
(
Tile(char, value)
, count
)
}
).toMap
val full: Bag =
new Bag(countByTileFull)
}
case class Bag private [Bag] (countByTile: Map[Tile, Int]) {
require(countByTile.nonEmpty, "tilesByChar.nonEmpty must be true")
require(!countByTile.exists(_._2 < 1), s"all counts in countByTile [${countByTile.toList.sortBy(_._1.char).map {case (tile, count) => s"${tile.char}->$count" }.mkString(",")}] must be greater than 0")
val tilesByChar: Map[Char, List[Tile]] =
countByTile.map {
case (tile, count) =>
(
tile.char
, List.fill(count)(tile)
)
}
val tilesPresentList: List[Tile] =
tilesByChar.toList.sortBy(-_._2.size).flatMap(_._2.sortBy(_.char))
lazy val tilesMissingSet: Set[Tile] =
Bag.full.countByTile.keySet.diff(countByTile.keySet)
val tilesByCount: Map[Int, Set[Tile]] =
countByTile
.groupBy(_._2)
.map {
case (count, countByTile2) =>
(count, countByTile2.keySet)
}
private def removeTileImpl(tile: Tile): (Option[Bag], Tile) = {
//assumes tile has been verified to be present by caller
val countByTileNew = {
val count =
countByTile(tile)
if (count > 1)
countByTile + (tile -> (count - 1))
else
countByTile - tile //got the last one
}
(
if (countByTileNew.nonEmpty)
Some(new Bag(countByTileNew))
else
None
, tile
)
}
def getTileRandom(random: Random = Random): (Option[Bag], Tile) =
removeTileImpl(tilesPresentList(random.nextInt(tilesPresentList.size)))
def getTilesRandom(quantity: Int, random: Random = Random): Try[(Option[Bag], List[Tile])] =
if (quantity <= tilesPresentList.size)
if (quantity == tilesPresentList.size)
Success((None, tilesPresentList))
else
Try(
(1 to quantity).foldLeft((this, List[Tile]())) {
case ((bag, tiles), index) =>
val (optionBagNew, tileNew) =
bag.getTileRandom()
optionBagNew match {
case Some(bagNew) =>
(bagNew, tileNew :: tiles)
case None =>
throw new IllegalStateException(s"internal error - should not receive None here")
}
}
).map {
case (bag, tiles) =>
(Some(bag), tiles)
}
else
Failure(new IllegalArgumentException(s"quantity [$quantity] must be less than or equal to tilesPresentList.size [${tilesPresentList.size}]"))
def removeTile(char: Char): Try[(Option[Bag], Tile)] =
removeTiles(char.toString).map { case (optionBag, tiles) => (optionBag, tiles.head) }
def removeTiles(chars: String): Try[(Option[Bag], List[Tile])] = {
val charAndOptionTiles =
chars.toUpperCase.map(char => (char, tilesByChar.get(char)))
if (!charAndOptionTiles.exists { case (char, optionTiles) => optionTiles.isEmpty})
removeTiles(
charAndOptionTiles.collect {
case (char, Some(tiles)) =>
tiles.head
}.toList
)
else {
val messages =
charAndOptionTiles.collect {
case (char, None) =>
char
}.sorted.mkString(",")
val plural =
if (charAndOptionTiles.size > 1) "s" else ""
Failure(new IllegalArgumentException(s"unable to convert char$plural [$chars] to Tile$plural - $messages"))
}
}
def removeTile(tile: Tile): Try[(Option[Bag], Tile)] = {
if (countByTile.keySet.contains(tile))
Success(removeTileImpl(tile))
else
Failure(new IllegalArgumentException(s"tile [${tile.char}] is not available in countByTile.keySet [${countByTile.keySet.map(_.char).toList.sorted.mkString(",")}]"))
}
def removeTiles(tiles: List[Tile]): Try[(Option[Bag], List[Tile])] =
if (tiles.size <= tilesPresentList.size) {
def recursive(tilesRemaining: List[Tile], optionBagNew: Option[Bag], accumulator: List[Tile] = Nil): Try[(Option[Bag], List[Tile])] =
if (tilesRemaining.isEmpty)
Success((optionBagNew, accumulator))
else
optionBagNew match {
case Some(bagNew) =>
val tryOptionBagNewAndTile =
bagNew.removeTile(tilesRemaining.head)
tryOptionBagNewAndTile match {
case Success((optionBagNew2, tile)) =>
recursive(tilesRemaining.tail, optionBagNew2, tile :: accumulator)
case Failure(e) =>
Failure(e)
}
case None =>
Failure(new IllegalStateException(s"internal error - should not receive None here"))
}
recursive(tiles, Some(this))
}
else
Failure(new IllegalArgumentException(s"tiles.size [${tiles.size}] must be less than or equal to tilesPresentList.size [${tilesPresentList.size}]"))
}
package org.public_domain.scrabble
import scala.util.{Failure, Success}
object Main extends App {
//URL for article defining problem challenge:
// https://dzone.com/articles/java-code-challenge-scrabble-sets
def computeCharsByCount(bag: Bag): String = {
(
if (bag.tilesMissingSet.nonEmpty)
bag.tilesByCount + (0 -> bag.tilesMissingSet)
else
bag.tilesByCount
)
.toList
.sortBy(-_._1)
.map {
case (count, tiles) =>
s"$count: ${tiles.toList.map(_.char).sorted.mkString(", ")}"
}
.mkString("\n")
}
val inputs =
List(
"PQAREIOURSTHGWIOAE_" //article test case # 1
, "LQTOONOEFFJZT" //article test case # 2
, "AXHDRUIOR_XHJZUQEE" //article test case # 3
, "q"
, "b"
, "bB"
, "pqareioursthgwioae_"
, "lqtoonoeffjzt"
, "axhdruior_xhjzuqee"
, "EEEEEEEEEEEEAAAAAAAAAIIIIIIIIIOOOOOOOONNNNNNTTTTTTRRRRRRUUUULLLLDDDDSSSSGGGYYFFMMVVBBPP__CCHHWWXJQKZ"
, "EEEEEEEEEEEEAAAAAAAAAIIIIIIIIIOOOOOOOONNNNNNTTTTTTRRRRRRUUUULLLLDDDDSSSSGGGYYFFMMVVBBPP__CCHHWWXJQKZA"
, "EEEEEEEEEEEEAAAAAAAAAIIIIIIIIIOOOOOOOONNNNNNTTTTTTRRRRRRUUUULLLLDDDDSSSSGGGYYFFMMVVBBPP__CCHHWWXJQK"
)
val results =
inputs.map(
chars =>
(
chars
, Bag.full.removeTiles(chars) match {
case Success((optionBag, tiles)) =>
optionBag match {
case Some(bag) =>
computeCharsByCount(bag)
case None =>
"0: " + Bag.full.countByTile.keySet.toList.map(_.char).sorted.mkString(", ")
}
case Failure(e) =>
if (e.getMessage.contains(" is not available in countByTile.keySet [")) {
val char =
e.getMessage.drop("tile [".length).head
s"Invalid input. More $char's have been taken from the bag than possible."
}
else
s"Invalid input. ${e.getMessage.capitalize}"
}
)
)
val resultsAsString =
results.map {
case(chars, result) =>
s"$chars:\n$result"
}.mkString("\n\n")
println(resultsAsString)
}
package org.public_domain.scrabble
case class Tile(char: Char, value: Int)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment