pressletter 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +18 -0
- data/.rspec +2 -0
- data/Gemfile +12 -0
- data/Guardfile +24 -0
- data/LICENSE.txt +22 -0
- data/README.md +55 -0
- data/Rakefile +1 -0
- data/assets/dictionary.txt +386264 -0
- data/bin/pressletter +7 -0
- data/lib/pressletter.rb +16 -0
- data/lib/pressletter/core/create_letters.rb +19 -0
- data/lib/pressletter/core/find_words.rb +23 -0
- data/lib/pressletter/core/load_dictionary.rb +5 -0
- data/lib/pressletter/core/print_words.rb +5 -0
- data/lib/pressletter/core/solve.rb +6 -0
- data/lib/pressletter/core/sort_words.rb +11 -0
- data/lib/pressletter/default_config.rb +5 -0
- data/lib/pressletter/shell/api.rb +25 -0
- data/lib/pressletter/shell/cli.rb +15 -0
- data/lib/pressletter/shell/reads_input.rb +10 -0
- data/lib/pressletter/shell/writes_output.rb +7 -0
- data/lib/pressletter/values/config.rb +3 -0
- data/lib/pressletter/values/dictionary.rb +10 -0
- data/lib/pressletter/values/letters.rb +18 -0
- data/lib/pressletter/values/words.rb +11 -0
- data/lib/pressletter/version.rb +3 -0
- data/pressletter.gemspec +32 -0
- data/spec/fixtures/simple_dict.txt +7 -0
- data/spec/lib/pressletter/core/create_letters_spec.rb +14 -0
- data/spec/lib/pressletter/core/find_words_spec.rb +20 -0
- data/spec/lib/pressletter/core/load_dictionary_spec.rb +23 -0
- data/spec/lib/pressletter/core/print_words_spec.rb +12 -0
- data/spec/lib/pressletter/core/solve_spec.rb +8 -0
- data/spec/lib/pressletter/core/sort_words_spec.rb +7 -0
- data/spec/lib/pressletter/shell/api_spec.rb +26 -0
- data/spec/lib/pressletter/shell/cli_spec.rb +22 -0
- data/spec/spec_helper.rb +28 -0
- metadata +145 -0
data/bin/pressletter
ADDED
data/lib/pressletter.rb
ADDED
@@ -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,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,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,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
|
data/pressletter.gemspec
ADDED
@@ -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,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,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
|
data/spec/spec_helper.rb
ADDED
@@ -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
|