pressletter 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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