pressletter 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. data/.gitignore +18 -0
  2. data/.rspec +2 -0
  3. data/Gemfile +12 -0
  4. data/Guardfile +24 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +55 -0
  7. data/Rakefile +1 -0
  8. data/assets/dictionary.txt +386264 -0
  9. data/bin/pressletter +7 -0
  10. data/lib/pressletter.rb +16 -0
  11. data/lib/pressletter/core/create_letters.rb +19 -0
  12. data/lib/pressletter/core/find_words.rb +23 -0
  13. data/lib/pressletter/core/load_dictionary.rb +5 -0
  14. data/lib/pressletter/core/print_words.rb +5 -0
  15. data/lib/pressletter/core/solve.rb +6 -0
  16. data/lib/pressletter/core/sort_words.rb +11 -0
  17. data/lib/pressletter/default_config.rb +5 -0
  18. data/lib/pressletter/shell/api.rb +25 -0
  19. data/lib/pressletter/shell/cli.rb +15 -0
  20. data/lib/pressletter/shell/reads_input.rb +10 -0
  21. data/lib/pressletter/shell/writes_output.rb +7 -0
  22. data/lib/pressletter/values/config.rb +3 -0
  23. data/lib/pressletter/values/dictionary.rb +10 -0
  24. data/lib/pressletter/values/letters.rb +18 -0
  25. data/lib/pressletter/values/words.rb +11 -0
  26. data/lib/pressletter/version.rb +3 -0
  27. data/pressletter.gemspec +32 -0
  28. data/spec/fixtures/simple_dict.txt +7 -0
  29. data/spec/lib/pressletter/core/create_letters_spec.rb +14 -0
  30. data/spec/lib/pressletter/core/find_words_spec.rb +20 -0
  31. data/spec/lib/pressletter/core/load_dictionary_spec.rb +23 -0
  32. data/spec/lib/pressletter/core/print_words_spec.rb +12 -0
  33. data/spec/lib/pressletter/core/solve_spec.rb +8 -0
  34. data/spec/lib/pressletter/core/sort_words_spec.rb +7 -0
  35. data/spec/lib/pressletter/shell/api_spec.rb +26 -0
  36. data/spec/lib/pressletter/shell/cli_spec.rb +22 -0
  37. data/spec/spec_helper.rb +28 -0
  38. metadata +145 -0
data/bin/pressletter ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', 'lib')
4
+
5
+ require 'pressletter'
6
+
7
+ Pressletter::Shell::CLI.new.main
@@ -0,0 +1,16 @@
1
+ module Pressletter
2
+ module Shell
3
+ # This namespace is for the minimal interoperable imperative code
4
+ end
5
+
6
+ module Core
7
+ # This namespace is for the private functional code
8
+ end
9
+
10
+ module Values
11
+ # This namespace is for behavioral-less values which
12
+ # => serve as messages between core & shell and between shells.
13
+ end
14
+ end
15
+
16
+ Dir[File.join(File.dirname(__FILE__), "**", "*.rb")].each {|file| require file }
@@ -0,0 +1,19 @@
1
+ module Pressletter::Core
2
+ def create_letters(input)
3
+ Pressletter::Values::Letters.new(
4
+ ensure_alphabetical(
5
+ input.chomp.split('').
6
+ map { |c| c.upcase }.
7
+ reject { |c| c == ' ' }.compact.sort
8
+ )
9
+ )
10
+ end
11
+
12
+ private
13
+
14
+ def ensure_alphabetical(letters)
15
+ raise "Letters only!" if letters.any? {|l| l =~ /[^A-Z]/}
16
+ letters
17
+ end
18
+
19
+ end
@@ -0,0 +1,23 @@
1
+ module Pressletter::Core
2
+ def find_words(dictionary, letters)
3
+ Pressletter::Values::Words.new(
4
+ dictionary.as_array.map do |word|
5
+ word if letters_can_buy?(letters, word)
6
+ end.compact
7
+ )
8
+ end
9
+
10
+ private
11
+
12
+ def letters_can_buy?(letters, word)
13
+ letters = letters.as_hash.dup
14
+ word.dup.split('').all? do |char|
15
+ decrement_char!(char, letters)
16
+ end
17
+ end
18
+
19
+ def decrement_char!(char, letters)
20
+ letters[char] -= 1 if letters[char] && letters[char] > 0
21
+ end
22
+
23
+ end
@@ -0,0 +1,5 @@
1
+ module Pressletter::Core
2
+ def load_dictionary(location)
3
+ Dictionary.new(File.read(location).split("\n"))
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ module Pressletter::Core
2
+ def print_words(words)
3
+ words.as_array.join("\n")
4
+ end
5
+ end
@@ -0,0 +1,6 @@
1
+ module Pressletter::Core
2
+ def solve(config, letters)
3
+ sort_words(find_words(load_dictionary(config.dictionary_location), letters))
4
+ end
5
+ end
6
+
@@ -0,0 +1,11 @@
1
+ module Pressletter::Core
2
+ def sort_words(words)
3
+ Pressletter::Values::Words.new(words.as_array.sort(&method(:size_then_alphabet)))
4
+ end
5
+
6
+ private
7
+
8
+ def size_then_alphabet(word_1, word_2)
9
+ [-word_1.size, word_1] <=> [-word_2.size, word_2]
10
+ end
11
+ end
@@ -0,0 +1,5 @@
1
+ module Pressletter
2
+ def self.default_config
3
+ Pressletter::Values::Config.new(File.expand_path("../../../assets/dictionary.txt", __FILE__))
4
+ end
5
+ end
@@ -0,0 +1,25 @@
1
+ module Pressletter
2
+ module Shell
3
+ class API
4
+ include Pressletter::Core
5
+
6
+ def initialize(config=Pressletter::default_config)
7
+ @config = config
8
+ end
9
+
10
+ def solve_letters(letters)
11
+ solve(@config, create_letters(ensure_string(letters))).as_array
12
+ end
13
+
14
+ private
15
+
16
+ def ensure_string(input)
17
+ input.respond_to?(:join) ? input.join : input
18
+ end
19
+ end
20
+ end
21
+
22
+ def self.solve(letters, config=Pressletter::default_config)
23
+ Pressletter::Shell::API.new(config).solve_letters(letters)
24
+ end
25
+ end
@@ -0,0 +1,15 @@
1
+ module Pressletter::Shell
2
+ class CLI
3
+ include Pressletter::Core
4
+
5
+ def initialize(config = Pressletter::default_config, reads_input = ReadsInput.new, writes_output = WritesOutput.new)
6
+ @config = config
7
+ @reads_input = reads_input
8
+ @writes_output = writes_output
9
+ end
10
+
11
+ def main
12
+ @writes_output.write(print_words(solve(@config, create_letters(@reads_input.read))))
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,10 @@
1
+ module Pressletter::Shell
2
+ class ReadsInput
3
+ def read
4
+ ARGV[0] || begin
5
+ puts "Please enter candidate Letterpress letters, then press <return>:\n"
6
+ gets
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,7 @@
1
+ module Pressletter::Shell
2
+ class WritesOutput
3
+ def write(s)
4
+ puts(s)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,3 @@
1
+ module Pressletter::Values
2
+ Config = Struct.new(:dictionary_location)
3
+ end
@@ -0,0 +1,10 @@
1
+ class Dictionary
2
+ def initialize(dictionary)
3
+ @dictionary = dictionary
4
+ end
5
+
6
+ def as_array
7
+ @dictionary
8
+ end
9
+ end
10
+
@@ -0,0 +1,18 @@
1
+ module Pressletter::Values
2
+ class Letters
3
+ def initialize(letters)
4
+ @letters = letters
5
+ end
6
+
7
+ def as_array
8
+ @letters
9
+ end
10
+
11
+ def as_hash
12
+ @hash ||= @letters.inject({}) do |h,l|
13
+ h[l] = h[l] ? h[l] + 1 : 1
14
+ h
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,11 @@
1
+ module Pressletter::Values
2
+ class Words
3
+ def initialize(words)
4
+ @words = words
5
+ end
6
+
7
+ def as_array
8
+ @words
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,3 @@
1
+ module Pressletter
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,32 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'pressletter/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "pressletter"
8
+ gem.version = Pressletter::VERSION
9
+ gem.authors = ["Justin Searls"]
10
+ gem.email = ["justin@testdouble.com"]
11
+ gem.description = <<-EOF
12
+ pressletter is a tool for solving Letterpress puzzles. Using the
13
+ `pressletter` binary like this:
14
+
15
+ $ pressletter eiptctbntymeiphoxvitkmzib
16
+
17
+ The argument is a list of all the letters on a pressletter board
18
+ and will yield an ordered list (from longest to shorted) of all
19
+ legal words that can be made with the provided letters.
20
+ EOF
21
+ gem.summary = %q{A tool for solving Letterpress puzzles}
22
+ gem.homepage = "https://github.com/searls/pressletter"
23
+
24
+ gem.files = `git ls-files`.split($/)
25
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
26
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
27
+ gem.require_paths = ["lib"]
28
+
29
+ gem.add_development_dependency "rspec", "~> 2.12.0"
30
+ gem.add_development_dependency "rspec-given", "~> 2.2.0"
31
+ gem.add_development_dependency "gimme", "~> 0.3.4"
32
+ end
@@ -0,0 +1,7 @@
1
+ COT
2
+ COPTIC
3
+ EPITOME
4
+ MEMITIC
5
+ TENT
6
+ TINT
7
+ ZEBRAPANTS
@@ -0,0 +1,14 @@
1
+ describe "Pressletter::Core#create_letters" do
2
+ include Pressletter::Core
3
+
4
+ context "several letters" do
5
+ When(:result) { create_letters("c a ba\n") }
6
+ Then { result.as_array.should == ["A", "A", "B", "C"] }
7
+ Then { result.as_hash.should == { "A" => 2, "B" => 1, "C" => 1 }}
8
+ end
9
+
10
+ context "containing a non-alpha character" do
11
+ Then { lambda { create_letters("1") }.should raise_error "Letters only!" }
12
+ end
13
+
14
+ end
@@ -0,0 +1,20 @@
1
+ describe "Pressletter::Core#find_words" do
2
+ include Pressletter::Core
3
+
4
+ context "one word dictionary" do
5
+ Given(:dictionary) { Dictionary.new(["FOOD"]) }
6
+ context "and we've got the letters" do
7
+ Given(:letters) { Pressletter::Values::Letters.new(["D","F","O","O"]) }
8
+ When(:result) { find_words(dictionary, letters) }
9
+ Then { result.as_array.should == ["FOOD"] }
10
+ end
11
+
12
+ context "and we don't got the letters" do
13
+ Given(:letters) { Pressletter::Values::Letters.new(["D","F","O"]) }
14
+ When(:result) { find_words(dictionary, letters) }
15
+ Then { result.as_array.should == [] }
16
+ end
17
+ end
18
+
19
+
20
+ end
@@ -0,0 +1,23 @@
1
+ describe "Pressletter::Core#load_dictionary" do
2
+ include Pressletter::Core
3
+
4
+ Given(:content) do
5
+ <<-TXT.gsub /^\s+/, ""
6
+ A
7
+ ABC
8
+ DOG
9
+ PANDA
10
+ TXT
11
+ end
12
+ Given(:path) { tempfile_location_containing(content) }
13
+ When(:result) { load_dictionary(path) }
14
+ Then { result.as_array.should == ["A", "ABC", "DOG", "PANDA"]}
15
+
16
+ def tempfile_location_containing(content)
17
+ require 'tempfile'
18
+ f = Tempfile.new("not-unique")
19
+ f.write(content)
20
+ f.close
21
+ f.path
22
+ end
23
+ end
@@ -0,0 +1,12 @@
1
+ describe "Pressletter::Core#print_words" do
2
+ include Pressletter::Core
3
+
4
+ Given(:words) { Pressletter::Values::Words.new(["AL", "BAR", "ZEBRA"]) }
5
+ When(:result) { print_words(words) }
6
+ Then do
7
+ result.should == "AL\n" +
8
+ "BAR\n" +
9
+ "ZEBRA"
10
+
11
+ end
12
+ end
@@ -0,0 +1,8 @@
1
+ describe "Pressletter::Core#solve" do
2
+ include Pressletter::Core
3
+
4
+ Given(:config) { Pressletter::Values::Config.new(File.expand_path("../../../../fixtures/simple_dict.txt", __FILE__)) }
5
+ Given(:letters) { Pressletter::Values::Letters.new("eiptctbntymeiphoxvitkmzib".upcase.split('')) }
6
+ When(:result) { solve(config, letters) }
7
+ Then { result.as_array.should == ["EPITOME", "MEMITIC", "TENT", "TINT", "COT"] }
8
+ end
@@ -0,0 +1,7 @@
1
+ describe "Pressletter::Core#sort_words" do
2
+ include Pressletter::Core
3
+
4
+ Given(:words) { Pressletter::Values::Words.new(["AL", "BAR", "CAR", "ZEBRA"]) }
5
+ When(:result) { sort_words(words) }
6
+ Then { result.as_array.should == ["ZEBRA", "BAR", "CAR", "AL"] }
7
+ end
@@ -0,0 +1,26 @@
1
+ module Pressletter::Shell
2
+ describe "ruby API" do
3
+ shared_examples_for "API methods" do
4
+ Given(:letters) { "eiptctbntymeiphoxvitkmzib".split('') }
5
+ Then { result.should == ["EPITOME", "MEMITIC", "TENT", "TINT", "COT"] }
6
+ end
7
+
8
+ Given(:config) { Pressletter::Values::Config.new(File.expand_path("../../../../fixtures/simple_dict.txt", __FILE__)) }
9
+
10
+ describe API do
11
+ subject { API.new(config) }
12
+
13
+ describe "#solve" do
14
+ it_behaves_like "API methods" do
15
+ When(:result) { subject.solve_letters(letters) }
16
+ end
17
+ end
18
+ end
19
+
20
+ describe "top-level DSL" do
21
+ it_behaves_like "API methods" do
22
+ When(:result) { Pressletter.solve(letters, config) }
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,22 @@
1
+ module Pressletter::Shell
2
+ describe CLI do
3
+ Given(:config) { Pressletter::Values::Config.new(File.expand_path("../../../../fixtures/simple_dict.txt", __FILE__)) }
4
+ Given(:reads_input) { gimme(ReadsInput) }
5
+ Given(:writes_output) { gimme(WritesOutput) }
6
+
7
+ subject { CLI.new(config, reads_input, writes_output) }
8
+
9
+ describe "#main" do
10
+ Given { give(reads_input).read {"eiptctbntymeiphoxvitkmzib"} }
11
+ When { subject.main }
12
+ Then do
13
+ verify(writes_output).write contains "EPITOME\n"+
14
+ "MEMITIC\n" +
15
+ "TENT\n" +
16
+ "TINT\n" +
17
+ "COT"
18
+ end
19
+
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,28 @@
1
+ require 'rspec'
2
+ require 'gimme'
3
+ require 'rspec/given'
4
+ require 'pressletter'
5
+
6
+
7
+ module GimmeMatchers
8
+ class Contains
9
+ def initialize(expected)
10
+ @expected = expected
11
+ end
12
+ def matches?(actual)
13
+ actual.include?(@expected)
14
+ end
15
+ end
16
+ def contains(expected)
17
+ Contains.new(expected)
18
+ end
19
+ end
20
+ include GimmeMatchers
21
+
22
+ RSpec.configure do |config|
23
+ config.mock_framework = Gimme::RSpecAdapter
24
+
25
+ config.after(:each) do
26
+ Gimme.reset
27
+ end
28
+ end