| 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 |