digits_solver 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b539861e0a6120403802c85ac9fb08960aa17c7064687064269a2b59b6213f3d
4
+ data.tar.gz: c0a746b1c33a7866ed10b39e51ae672806f723b01336b81f63b0563a3b45fd05
5
+ SHA512:
6
+ metadata.gz: dd7f9d5a8c08a0ad227aa02f1adbdf12b331b69a9e068f8a1586d9e2867766b5bd6eed8c1d7bae93b57cabb310d6f637397aedf9471a5a2aecf1e07b88bea120
7
+ data.tar.gz: 0dcb86378227e3184e92b63af05fcd3b2cf210d18e0c3e1e4d92bbe973f333c68a47567ae2ba9bd023906fe673426353052e7a0f86d2c2752932e02875c646d3
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,13 @@
1
+ AllCops:
2
+ TargetRubyVersion: 2.6
3
+
4
+ Style/StringLiterals:
5
+ Enabled: true
6
+ EnforcedStyle: double_quotes
7
+
8
+ Style/StringLiteralsInInterpolation:
9
+ Enabled: true
10
+ EnforcedStyle: double_quotes
11
+
12
+ Layout/LineLength:
13
+ Max: 120
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in digits_solver.gemspec
6
+ gemspec
7
+
8
+ gem "rake", "~> 13.0"
9
+
10
+ gem "rspec", "~> 3.0"
11
+
12
+ gem "rubocop", "~> 1.21"
data/README.md ADDED
@@ -0,0 +1,34 @@
1
+ # DigitsSolver
2
+
3
+ This is basically a coding exercice.
4
+
5
+ The goal of this gem is to solve puzzles à-la [NY Times Digits game](https://www.nytimes.com/games/digits). You may
6
+ actually know this game under a different name. In France this looks like the calculation part of a 40 year old TV
7
+ show named 'Les chiffres et les lettres'...
8
+
9
+ This gem implements a brute-force approach to solve the problem but is ready to host alternative strategies.
10
+
11
+
12
+ ## Installation
13
+
14
+ $ gem install digits_solver
15
+
16
+ ## Usage
17
+
18
+ This gem provides an executable named `find_nydigits_solutions`.
19
+
20
+ * First argument is the target number.
21
+ * All subsequent arguments are the numbers to be used in calculations to reach the target.
22
+
23
+ ex:
24
+
25
+ ```
26
+ $ find_nydigits_solutions 437 3 5 7 13 20 25
27
+ Trying to find 437
28
+ A best solution has been found (total of 67 found).
29
+ Solved in 4 operations:
30
+ => 5 * 7 = 35
31
+ => 35 - 13 = 22
32
+ => 22 * 20 = 440
33
+ => 440 - 3 = 437
34
+ ```
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/digits_solver/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "digits_solver"
7
+ spec.version = DigitsSolver::VERSION
8
+ spec.authors = ["Laurent B."]
9
+ spec.email = ["lbnetid+rb@gmail.com"]
10
+
11
+ spec.summary = "Solves NYTimes Digits puzzle."
12
+ spec.description = "Finds solutions to the NYTimes Digits game."
13
+ spec.homepage = "https://gitlab.com/coding_exercices/digits_solver"
14
+ spec.required_ruby_version = ">= 2.6.0"
15
+
16
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
17
+
18
+ spec.metadata["homepage_uri"] = spec.homepage
19
+ spec.metadata["source_code_uri"] = spec.homepage
20
+
21
+ # Specify which files should be added to the gem when it is released.
22
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
23
+ spec.files = Dir.chdir(__dir__) do
24
+ `git ls-files -z`.split("\x0").reject do |f|
25
+ (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|circleci)|appveyor)})
26
+ end
27
+ end
28
+ spec.bindir = "exe"
29
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
30
+ spec.require_paths = ["lib"]
31
+
32
+ # Uncomment to register a new dependency of your gem
33
+ # spec.add_dependency "example-gem", "~> 1.0"
34
+ spec.add_development_dependency 'pry'
35
+ spec.add_development_dependency 'rspec'
36
+
37
+ # For more information and examples about making a new gem, check out our
38
+ # guide at: https://bundler.io/guides/creating_gem.html
39
+ end
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env ruby
2
+ # coding: utf-8
3
+
4
+ require 'rubygems'
5
+ require 'bundler/setup'
6
+ require 'digits_solver'
7
+
8
+ def help(out = STDOUT)
9
+ out.puts <<EOM
10
+ To run this program:
11
+
12
+ find_nydigits_solution <number to find> <space separated list of numbers>
13
+
14
+ This program will solve if possible the NY Digits puzzle.
15
+ EOM
16
+ end
17
+
18
+ begin
19
+ raise DigitsSolver::Error, 'Invalid parameters' unless ARGV.size >= 3 && ARGV.size <= 7
20
+
21
+ target = ARGV.shift.to_i
22
+ draw = ARGV.map do |val|
23
+ raise DigitsSolver::Error, 'Invalid parameters (candidate figures) ! ' unless val == val.to_i.to_s
24
+
25
+ val.to_i
26
+ end
27
+ puts "Trying to find #{target}"
28
+ solutions = DigitsSolver.solve_for target, *draw
29
+ raise DigitsSolver::Error, %q[Couldn't find any solution !] if solutions.sorted_solutions.empty?
30
+
31
+ puts "A best solution has been found (total of #{solutions.size} possible solutions found)."
32
+ puts solutions.best_solution
33
+ rescue => e
34
+ STDERR.puts e.message
35
+ help STDERR
36
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+ module DigitsSolver
3
+ class Error < StandardError ; end
4
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+ module DigitsSolver
3
+ class ProblemStatement
4
+
5
+ attr_reader :target_number
6
+
7
+ def draw
8
+ @draw.dup
9
+ end
10
+
11
+ def initialize(target_number_or_problem_statement, *draw)
12
+ @target_number, @draw = if target_number_or_problem_statement.is_a? DigitsSolver::ProblemStatement
13
+ [target_number_or_problem_statement.target_number, target_number_or_problem_statement.draw]
14
+ else
15
+ [target_number_or_problem_statement, draw]
16
+ end
17
+ DigitsSolver.logger.info "The target is #{target_number}"
18
+ DigitsSolver.logger.info "The draw is #{draw.inspect}"
19
+ end
20
+
21
+ def max_operations_number
22
+ @max_operations_number ||= @draw.size - 1
23
+ end
24
+
25
+ end
26
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+ module DigitsSolver
3
+
4
+ class Solution
5
+
6
+ include DigitsSolver::Strategies::Base
7
+
8
+ attr_reader :problem_statement, :operands, :operations_to_apply
9
+
10
+ def initialize(problem_statement, operands, operations_to_apply)
11
+ raise DigitsSolver::Error, "Invalid problem statement" unless problem_statement.is_a? DigitsSolver::ProblemStatement
12
+ unless operands.size == operations_to_apply.size + 1
13
+ raise DigitsSolver::Error, "Invalid solution #{operands.inspect} => #{operations_to_apply.inspect}"
14
+ end
15
+
16
+ DigitsSolver.logger.info "New solution: #{self.inspect}"
17
+
18
+ @problem_statement = problem_statement
19
+ @operands = operands
20
+ @operations_to_apply = operations_to_apply
21
+ end
22
+
23
+ def ==(other_solution)
24
+ (operations_to_apply == other_solution.operations_to_apply) && (operands == other_solution.operands)
25
+ end
26
+
27
+ def to_s
28
+ res = ["Solved in #{operations_to_apply.size} operation#{operations_to_apply.size <= 1 ? '' : 's'}:"]
29
+ res.concat to_operation_lines
30
+ res.join "\n"
31
+ end
32
+
33
+ def to_operation_lines
34
+ res = []
35
+ operations_to_apply.each.with_index.reduce(operands.first) do |acc, (operation, idx)|
36
+ computed_result = apply_operation_to_operands operation, acc, operands[idx + 1]
37
+ res << format(' => %u %s %u = %u',
38
+ acc,
39
+ DigitsSolver::Strategies::Base::OPERATIONS[operation],
40
+ operands[idx + 1],
41
+ computed_result)
42
+ computed_result
43
+ end
44
+ res
45
+ end
46
+
47
+ def pretty_print(pp)
48
+ pp.object_address_group(self) do
49
+ pp.breakable
50
+ pp.text "Solution: '#{to_evaluable_code} = #{problem_statement.target_number}'"
51
+ pp.text ','
52
+ pp.breakable
53
+ pp.seplist(self.instance_variables) do |v|
54
+ pp.text "#{v}="
55
+ pp.pp self.instance_variable_get v
56
+ end
57
+ end
58
+ end
59
+
60
+ def to_evaluable_code
61
+ res = operations_to_apply.each.with_index.reduce(operands.first) do |res, (operation, idx)|
62
+ op1 = res
63
+ op2 = operands[idx + 1]
64
+ format_string = operation == :multiply ? '%s %s %u' : '(%s %s %u)'
65
+ format(format_string, op1, DigitsSolver::Strategies::Base::OPERATIONS[operation], op2)
66
+ end
67
+ res[-1] == ')' ? res[1...-1] : res
68
+ end
69
+
70
+ end
71
+
72
+ end
@@ -0,0 +1,76 @@
1
+ module DigitsSolver
2
+
3
+ class SolutionSet
4
+
5
+ include Enumerable
6
+
7
+ DEFAULT_STRATEGY = DigitsSolver::Strategies::BruteForce
8
+ attr_reader :problem_statement, :strategy
9
+
10
+ class << self
11
+
12
+ def solve_for(problem_statement, strategy: DEFAULT_STRATEGY)
13
+ extend strategy
14
+ solutions = solve(problem_statement)
15
+ new problem_statement, solutions, strategy
16
+ end
17
+
18
+ end
19
+
20
+ def each(&block)
21
+ solutions.each(&block)
22
+ end
23
+
24
+ def sorted_solutions
25
+ indexed_solutions.keys
26
+ .sort { |a, b| a.size <=> b.size }
27
+ .map { |k| indexed_solutions[k] }
28
+ .flatten
29
+ end
30
+
31
+ def best_solution(nb = 1)
32
+ res = indexed_solutions.keys
33
+ .sort { |a, b| a.size <=> b.size }
34
+ .take(nb)
35
+ .map { |k| indexed_solutions[k] }
36
+ .flatten
37
+ .take(nb)
38
+ nb == 1 ? res.first : res
39
+ end
40
+ alias best_solutions best_solution
41
+
42
+ def size
43
+ solutions.size
44
+ end
45
+
46
+ private
47
+
48
+ attr_reader :solutions, :indexed_solutions
49
+
50
+ def <<(solution)
51
+ return nil if solutions.include? solution
52
+
53
+ solutions << solution
54
+ indexed_solutions[solution.operands] ||= []
55
+ indexed_solutions[solution.operands] << solution
56
+ solutions
57
+ end
58
+
59
+ def initialize(problem_statement, solutions, strategy)
60
+ raise DigitsSolver::Error, "Invalid problem statement" unless problem_statement.is_a? DigitsSolver::ProblemStatement
61
+
62
+ @problem_statement = problem_statement
63
+ @strategy = strategy
64
+
65
+ @indexed_solutions = {}
66
+ @solutions = []
67
+
68
+ solutions.each do |solution|
69
+ self << solution
70
+ end
71
+
72
+ end
73
+
74
+ end
75
+
76
+ end
@@ -0,0 +1,77 @@
1
+ module DigitsSolver
2
+ module Strategies
3
+
4
+ module Base
5
+
6
+ class OperationError < StandardError; end
7
+
8
+ OPERATIONS = {
9
+ plus: :+,
10
+ minus: :-,
11
+ multiply: :*,
12
+ divide: :/
13
+ }.freeze
14
+
15
+ def solve(_)
16
+ raise DigitsSolver::Error 'You should not be there !'
17
+ end
18
+
19
+ protected
20
+
21
+ def possible_operations_for_ordered_draw(max_operations_number)
22
+ nb_operations = OPERATIONS.size
23
+ ops_a = OPERATIONS.to_a
24
+ number_of_possibilities = nb_operations ** max_operations_number
25
+
26
+ DigitsSolver.logger.info "number of operations permutations: #{number_of_possibilities}, nb op: #{nb_operations}"
27
+
28
+ (0...number_of_possibilities).map do |i|
29
+ i.to_s(nb_operations)
30
+ .ljust(max_operations_number, '0')
31
+ .chars
32
+ .map { |operation_index_str| ops_a[operation_index_str.to_i].first }
33
+ end
34
+ end
35
+
36
+ def apply_operations_chain_to_ordered_draw(operations_chain, ordered_draw, problem_statement)
37
+ operations_chain.each.with_index.reduce(ordered_draw.first) do |acc, (operation, idx)|
38
+ cur_op_res = apply_operation_to_operands(operation, acc, ordered_draw[idx + 1])
39
+ if cur_op_res == problem_statement.target_number
40
+ ops = operations_chain.take(idx + 1)
41
+ operands = ordered_draw.take(idx + 2)
42
+ yield DigitsSolver::Solution.new(problem_statement, operands, ops)
43
+ break
44
+ end
45
+ cur_op_res
46
+ end
47
+ end
48
+
49
+ def apply_operation_to_operands (operation, operand1, operand2)
50
+ res = send(operation, operand1, operand2)
51
+ DigitsSolver.logger.debug "#{operand1} #{OPERATIONS[operation]} #{operand2} = #{res}"
52
+ res
53
+ end
54
+
55
+ def plus(a, b)
56
+ a + b
57
+ end
58
+
59
+ def minus(a, b)
60
+ raise DigitsSolver::Strategies::Base::OperationError, "#{a} - #{b} is not allowed as result would be negative" unless a >= b
61
+
62
+ a - b
63
+ end
64
+
65
+ def multiply(a, b)
66
+ a * b
67
+ end
68
+
69
+ def divide(a, b)
70
+ raise DigitsSolver::Strategies::Base::OperationError, "#{a} / #{b} is not allowed as result would not be an integer" unless a % b == 0
71
+
72
+ a / b
73
+ end
74
+
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,36 @@
1
+ module DigitsSolver
2
+ module Strategies
3
+
4
+ module BruteForce
5
+
6
+ include DigitsSolver::Strategies::Base
7
+
8
+ def solve(problem_statement)
9
+ solutions = []
10
+ draw = problem_statement.draw
11
+ possible_operations = possible_operations_for_ordered_draw problem_statement.max_operations_number
12
+ # DigitsSolver.logger.debug possible_operations.inspect
13
+
14
+ draw.permutation.map do |ordered_draw|
15
+
16
+ DigitsSolver.logger.debug ordered_draw.inspect
17
+ possible_operations.each do |operations_chain|
18
+ DigitsSolver.logger.debug operations_chain.inspect
19
+ begin
20
+ apply_operations_chain_to_ordered_draw(operations_chain, ordered_draw, problem_statement) do |valid_solution|
21
+ solutions << valid_solution
22
+ end
23
+ rescue DigitsSolver::Strategies::Base::OperationError => dsboe
24
+ DigitsSolver.logger.debug "#{ordered_draw.inspect} => #{operations_chain.inspect} is discarded because #{dsboe.message}"
25
+ end
26
+ end
27
+
28
+ end
29
+ DigitsSolver.logger.info "Solutions: #{solutions.inspect}"
30
+ solutions
31
+ end
32
+
33
+ end
34
+
35
+ end
36
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DigitsSolver
4
+ VERSION = '0.1.1'.freeze
5
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "digits_solver/version"
4
+ require_relative "digits_solver/error"
5
+ require_relative "digits_solver/problem_statement"
6
+ require_relative "digits_solver/strategies/base"
7
+ require_relative "digits_solver/strategies/brute_force"
8
+ require_relative "digits_solver/solution"
9
+ require_relative 'digits_solver/solution_set'
10
+
11
+ module DigitsSolver
12
+ class Error < StandardError; end
13
+
14
+ class DummyLogger
15
+ private
16
+
17
+ def respond_to_missing?(method_name, *_)
18
+ return true if %i[debug info warn error fatal].include? method_name
19
+
20
+ super
21
+ end
22
+
23
+ def method_missing(method_name, *args)
24
+ return if %i[debug info warn error fatal].include? method_name
25
+
26
+ super
27
+ end
28
+ end
29
+
30
+ # Your code goes here...
31
+
32
+ def self.solve_for(target_number, *draw)
33
+ problem_statement = DigitsSolver::ProblemStatement.new target_number, *draw
34
+ DigitsSolver::SolutionSet.solve_for problem_statement
35
+ end
36
+
37
+ def self.logger
38
+ @logger ||= DigitsSolver::DummyLogger.new
39
+ end
40
+
41
+ def self.logger=(logger)
42
+ @logger = logger
43
+ end
44
+ end
@@ -0,0 +1,4 @@
1
+ module DigitsSolver
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,90 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: digits_solver
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Laurent B.
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2023-05-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: pry
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ description: Finds solutions to the NYTimes Digits game.
42
+ email:
43
+ - lbnetid+rb@gmail.com
44
+ executables:
45
+ - find_nydigits_solutions
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - ".rspec"
50
+ - ".rubocop.yml"
51
+ - Gemfile
52
+ - README.md
53
+ - Rakefile
54
+ - digits_solver.gemspec
55
+ - exe/find_nydigits_solutions
56
+ - lib/digits_solver.rb
57
+ - lib/digits_solver/error.rb
58
+ - lib/digits_solver/problem_statement.rb
59
+ - lib/digits_solver/solution.rb
60
+ - lib/digits_solver/solution_set.rb
61
+ - lib/digits_solver/strategies/base.rb
62
+ - lib/digits_solver/strategies/brute_force.rb
63
+ - lib/digits_solver/version.rb
64
+ - sig/digits_solver.rbs
65
+ homepage: https://gitlab.com/coding_exercices/digits_solver
66
+ licenses: []
67
+ metadata:
68
+ allowed_push_host: https://rubygems.org
69
+ homepage_uri: https://gitlab.com/coding_exercices/digits_solver
70
+ source_code_uri: https://gitlab.com/coding_exercices/digits_solver
71
+ post_install_message:
72
+ rdoc_options: []
73
+ require_paths:
74
+ - lib
75
+ required_ruby_version: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: 2.6.0
80
+ required_rubygems_version: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ version: '0'
85
+ requirements: []
86
+ rubygems_version: 3.3.26
87
+ signing_key:
88
+ specification_version: 4
89
+ summary: Solves NYTimes Digits puzzle.
90
+ test_files: []