Last active
June 28, 2016 14:26
-
-
Save chaotic3quilibrium/51744cecc2d2202e4db97b972509f8df to your computer and use it in GitHub Desktop.
Java Code Challenge: Scrabble Sets - Scala Solution
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}]")) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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