digits_solver 0.1.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.
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: []