Total Complexity | 42 |
Total Lines | 213 |
Duplicated Lines | 0 % |
Complex classes like Term often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
1 | require 'ref' |
||
8 | # |
||
9 | class Term |
||
10 | include Functions |
||
11 | |||
12 | # @raise NotImplementedError Term is abstract class. |
||
13 | def initialize |
||
14 | raise NotImplementedError, 'Term cannot be initialized' |
||
15 | end |
||
16 | |||
17 | # disallow duplication |
||
18 | def initialize_copy(*) |
||
19 | raise TypeError, 'Term cannot be duplicated (immutable, not necessary)' |
||
20 | end |
||
21 | |||
22 | class << self |
||
23 | protected :new |
||
24 | end |
||
25 | |||
26 | attr_reader :terms |
||
27 | |||
28 | def and(*others) |
||
29 | others.unshift self |
||
30 | Term.get AndTerm, *others |
||
31 | end |
||
32 | |||
33 | alias_method :&, :and |
||
34 | |||
35 | def or(*others) |
||
36 | others.unshift self |
||
37 | Term.get OrTerm, *others |
||
38 | end |
||
39 | |||
40 | alias_method :|, :or |
||
41 | |||
42 | def not |
||
43 | Term.get NotTerm, self |
||
44 | end |
||
45 | |||
46 | alias_method :~, :not |
||
47 | alias_method :-@, :not |
||
48 | |||
49 | def then(other) |
||
50 | Term.get ThenTerm, self, other |
||
51 | end |
||
52 | |||
53 | alias_method :>>, :then |
||
54 | |||
55 | def to_s_in_term |
||
56 | to_s true |
||
57 | end |
||
58 | |||
59 | def to_nnf |
||
60 | if nnf? |
||
61 | self |
||
62 | else |
||
63 | Term.get self.class, *@terms.map(&:to_nnf) |
||
64 | end |
||
65 | end |
||
66 | |||
67 | def nnf? |
||
68 | false |
||
69 | end |
||
70 | |||
71 | def reduce |
||
72 | if reduced? |
||
73 | self |
||
74 | else |
||
75 | Term.get self.class, *@terms.map(&:reduce) |
||
76 | end |
||
77 | end |
||
78 | |||
79 | def reduced? |
||
80 | false |
||
81 | end |
||
82 | |||
83 | def to_cnf |
||
84 | reduce.to_cnf |
||
85 | end |
||
86 | |||
87 | def self.validate_terms(*terms) |
||
88 | raise ArgumentError, 'no terms given' if terms.empty? |
||
89 | terms.map do |term| |
||
90 | case term |
||
91 | when TrueClass |
||
92 | True |
||
93 | when FalseClass |
||
94 | False |
||
95 | when Term |
||
96 | term |
||
97 | else |
||
98 | raise TypeError, "#{term.class} cannot be treated as term" |
||
99 | end |
||
100 | end |
||
101 | end |
||
102 | |||
103 | private_class_method :validate_terms |
||
104 | |||
105 | def self.generate_cache |
||
106 | Ref::WeakValueMap.new |
||
107 | end |
||
108 | |||
109 | private_class_method :generate_cache |
||
110 | |||
111 | def self.cached(klass, *terms) |
||
112 | @table ||= generate_cache |
||
113 | key = klass.name + terms.map(&:object_id).join(',') |
||
114 | return @table[key] if @table[key] |
||
115 | ret = klass.__send__ :new, *terms |
||
116 | @table[key] = ret |
||
117 | # kick caching mechanism |
||
118 | ret.variables |
||
119 | ret.freeze |
||
120 | end |
||
121 | |||
122 | private_class_method :cached |
||
123 | |||
124 | def self.get(klass, *terms) |
||
125 | terms = validate_terms(*terms) |
||
126 | if klass == AndTerm || klass == OrTerm |
||
127 | terms = terms.map { |t| t.is_a?(klass) ? t.terms : t }.flatten |
||
128 | return terms[0] if terms.length == 1 |
||
129 | end |
||
130 | cached klass, *terms |
||
131 | end |
||
132 | |||
133 | # check if this term is a cnf term. |
||
134 | # @return [Boolean] false unless overridden. |
||
135 | def cnf? |
||
136 | false |
||
137 | end |
||
138 | |||
139 | # @return [Array] variables used by this term. |
||
140 | def variables |
||
141 | @variables ||= @terms.map(&:variables).flatten.uniq |
||
142 | end |
||
143 | |||
144 | def assign(trues, falses, variables = nil) |
||
145 | # contradicted assignment |
||
146 | raise ArgumentError, 'Contradicted assignment' unless (trues & falses).empty? |
||
147 | variables ||= trues | falses |
||
148 | assigned_terms = terms.map do |term| |
||
149 | if (term.variables & variables).empty? |
||
150 | term |
||
151 | else |
||
152 | term.assign(trues, falses, variables) |
||
153 | end |
||
154 | end |
||
155 | Term.get self.class, *assigned_terms |
||
156 | end |
||
157 | |||
158 | def assign_true(*variables) |
||
159 | assign variables, [] |
||
160 | end |
||
161 | |||
162 | def assign_false(*variables) |
||
163 | assign [], variables |
||
164 | end |
||
165 | |||
166 | def sat? |
||
167 | PropLogic.sat_solver.call(self) |
||
168 | end |
||
169 | |||
170 | # loop with each satisfied terms. |
||
171 | # @return [Enumerator] if block is not given. |
||
172 | # @return [nil] if block is given. |
||
173 | def each_sat |
||
174 | return to_enum(:each_sat) unless block_given? |
||
175 | sat_loop(self) do |sat, solver| |
||
176 | yield sat |
||
177 | negated_vars = sat.terms.map do |t| |
||
178 | t.is_a?(NotTerm) ? t.terms[0] : ~t |
||
179 | end |
||
180 | solver << PropLogic.all_or(*negated_vars) |
||
181 | end |
||
182 | end |
||
183 | |||
184 | def unsat? |
||
185 | sat? == false |
||
186 | end |
||
187 | |||
188 | def equiv?(other) |
||
189 | ((self | other) & (~self | ~other)).unsat? |
||
190 | end |
||
191 | |||
192 | private |
||
193 | |||
194 | # checking methods |
||
195 | |||
196 | def check_term_uniqueness |
||
197 | @is_reduced &&= (@terms.length == @terms.uniq.length) |
||
198 | end |
||
199 | |||
200 | def check_ambivalent_vars |
||
201 | return unless @is_reduced |
||
202 | term_by_class = @terms.group_by(&:class) |
||
203 | return if term_by_class[NotTerm].nil? || term_by_class[Variable].nil? |
||
204 | negated_variales = term_by_class[NotTerm].map { |t| t.terms[0] } |
||
205 | @is_reduced = false unless (negated_variales & term_by_class[Variable]).empty? |
||
206 | end |
||
207 | |||
208 | def check_nnf_reduced |
||
209 | @is_reduced = true |
||
210 | @is_nnf = @terms.all? do |term| |
||
211 | @is_reduced &&= !term.is_a?(Constant) && term.reduced? |
||
212 | @is_reduced &&= !term.is_a?(NotTerm) || term.terms[0].is_a?(Variable) |
||
213 | next true if @is_reduced |
||
214 | term.nnf? |
||
215 | end |
||
216 | return unless @is_reduced |
||
217 | check_term_uniqueness |
||
218 | check_ambivalent_vars |
||
219 | end |
||
220 | |||
221 | end |
||
223 |