activefacts 0.8.10 → 0.8.12

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.
Files changed (52) hide show
  1. data/Rakefile +3 -2
  2. data/bin/afgen +25 -23
  3. data/bin/cql +9 -8
  4. data/css/orm2.css +23 -3
  5. data/examples/CQL/CompanyDirectorEmployee.cql +1 -1
  6. data/examples/CQL/Diplomacy.cql +3 -3
  7. data/examples/CQL/Insurance.cql +27 -21
  8. data/examples/CQL/Metamodel.cql +12 -8
  9. data/examples/CQL/MetamodelNext.cql +172 -149
  10. data/examples/CQL/ServiceDirector.cql +17 -17
  11. data/examples/CQL/Supervision.cql +3 -5
  12. data/examples/CQL/WaiterTips.cql +1 -1
  13. data/examples/CQL/unit.cql +1 -1
  14. data/index.html +0 -0
  15. data/lib/activefacts/cql/FactTypes.treetop +41 -8
  16. data/lib/activefacts/cql/Language/English.treetop +10 -0
  17. data/lib/activefacts/cql/ObjectTypes.treetop +3 -1
  18. data/lib/activefacts/cql/Terms.treetop +34 -53
  19. data/lib/activefacts/cql/compiler.rb +1 -1
  20. data/lib/activefacts/cql/compiler/clause.rb +21 -8
  21. data/lib/activefacts/cql/compiler/constraint.rb +3 -1
  22. data/lib/activefacts/cql/compiler/entity_type.rb +1 -1
  23. data/lib/activefacts/cql/compiler/fact_type.rb +9 -3
  24. data/lib/activefacts/cql/compiler/join.rb +3 -0
  25. data/lib/activefacts/cql/compiler/value_type.rb +9 -4
  26. data/lib/activefacts/cql/parser.rb +11 -3
  27. data/lib/activefacts/generate/oo.rb +3 -3
  28. data/lib/activefacts/generate/ordered.rb +0 -4
  29. data/lib/activefacts/input/orm.rb +305 -250
  30. data/lib/activefacts/persistence/tables.rb +6 -0
  31. data/lib/activefacts/support.rb +18 -0
  32. data/lib/activefacts/version.rb +1 -1
  33. data/lib/activefacts/vocabulary/extensions.rb +59 -20
  34. data/lib/activefacts/vocabulary/metamodel.rb +23 -13
  35. data/lib/activefacts/vocabulary/verbaliser.rb +5 -3
  36. data/spec/absorption_spec.rb +3 -2
  37. data/spec/cql/comparison_spec.rb +1 -3
  38. data/spec/cql/context_spec.rb +1 -1
  39. data/spec/cql/entity_type_spec.rb +2 -2
  40. data/spec/cql/expressions_spec.rb +2 -4
  41. data/spec/cql/fact_type_matching_spec.rb +55 -3
  42. data/spec/cql/parser/fact_types_spec.rb +3 -0
  43. data/spec/cql/role_matching_spec.rb +8 -7
  44. data/spec/cql/samples_spec.rb +10 -2
  45. data/spec/cql_dm_spec.rb +2 -1
  46. data/spec/helpers/array_matcher.rb +18 -35
  47. data/spec/helpers/diff_matcher.rb +34 -13
  48. data/spec/helpers/file_matcher.rb +27 -43
  49. data/spec/helpers/string_matcher.rb +23 -33
  50. data/spec/norma_cql_spec.rb +1 -0
  51. data/spec/norma_tables_spec.rb +1 -2
  52. metadata +95 -102
@@ -58,14 +58,14 @@ Data Store is one Major-Version;
58
58
  Data Store is one Minor-Version;
59
59
  Data Store is one Revision-Version;
60
60
 
61
- Date is identified by DDMMYYYY where
62
- Date has one DDMMYYYY,
63
- DDMMYYYY is of at most one Date;
64
- Credential has at most one Expiration-Date;
61
+ Date Time YMDHMS is identified by MDYHMS where
62
+ Date Time YMDHMS has one MDYHMS,
63
+ MDYHMS is of at most one Date Time YMDHMS;
65
64
 
66
- Date Time is identified by MDYHMS where
67
- Date Time has one MDYHMS,
68
- MDYHMS is of at most one Date Time;
65
+ DateYYMMDD is identified by DDMMYYYY where
66
+ DateYYMMDD has one DDMMYYYY,
67
+ DDMMYYYY is of at most one DateYYMMDD;
68
+ Credential has at most one Expiration-DateYYMMDD;
69
69
 
70
70
  Duration is identified by Seconds where
71
71
  Duration has one Seconds,
@@ -126,16 +126,16 @@ Recurring Schedule wednesday;
126
126
 
127
127
  Satellite Message is identified by its Id;
128
128
  Satellite Message is designated for one Data Store;
129
- Satellite Message one Insertion-Date Time;
130
129
  Satellite Message has at most one Message Data;
131
130
  Satellite Message has at most one Message Header;
132
131
  Satellite Message is of at most one Provider Type;
133
132
  Satellite Message has one Serial Number;
133
+ Satellite Message has one insertion-Date Time YMDHMS;
134
134
 
135
135
  Subscription is identified by its Nr;
136
136
  Company has one Driver- Tech Subscription;
137
- Subscription has one Beginning-Date;
138
- Subscription has at most one Ending-Date;
137
+ Subscription has one Beginning-DateYYMMDD;
138
+ Subscription has at most one Ending-DateYYMMDD;
139
139
  Subscription is enabled;
140
140
 
141
141
  Switch is identified by its Id;
@@ -155,10 +155,10 @@ Switch is backup updates;
155
155
  Switch is send disabled;
156
156
  Switch is test vectors enabled;
157
157
 
158
- Time is identified by HHMMSS where
159
- Time has one HHMMSS,
160
- HHMMSS is of at most one Time;
161
- Recurring Schedule has one Start-Time;
158
+ TimeHMS is identified by HHMMSS where
159
+ TimeHMS has one HHMMSS,
160
+ HHMMSS is of at most one TimeHMS;
161
+ Recurring Schedule has one Start-TimeHMS;
162
162
 
163
163
  Transaction is identified by its Nr;
164
164
  Satellite Message has at most one Group-Transaction;
@@ -210,11 +210,11 @@ Data Store Service has one Subscription;
210
210
  */
211
211
  either Company is client or Company is vendor but not both;
212
212
  for each Credential exactly one of these holds:
213
- Data Store(2) requires Credential,
213
+ Data Store (2) requires Credential,
214
214
  Data Store Service requires Credential,
215
215
  Vendor requires Credential,
216
216
  Data Store File Host System has Internal-Credential,
217
- Data Store(1) has Internal-Credential;
217
+ Data Store (1) has Internal-Credential;
218
218
  for each Network exactly one of these holds:
219
219
  Network is used by Host System,
220
220
  Company has Origin-Network,
@@ -227,7 +227,7 @@ either Host System runs Switch or Data Store has Legacy-Switch but not both;
227
227
  for each Network at most one of these holds:
228
228
  Network is ip_single,
229
229
  Network has Ending-IP;
230
- Data Store Service (where Service is from Data Store) belongs to Client
230
+ Data Store Service (in which Service is from Data Store) belongs to Client
231
231
  if and only if
232
232
  Client has default Data Store;
233
233
  Network has Ending IP
@@ -28,12 +28,10 @@ CEO runs Company,
28
28
  /*
29
29
  * Constraints:
30
30
  */
31
- either Employee reports to Manager(2) or Employee is a Manager(1) that is a CEO that runs Company but not both;
31
+ either Employee reports to Manager(1) or Employee is a Manager(2) that is a CEO that runs Company but not both;
32
32
  Employee is a Manager that is a CEO that runs Company
33
33
  if and only if
34
34
  Employee works for Company;
35
-
36
- // This constraint cannot be expressed in NORMA until it adds explicit join paths:
37
- Employee(2) reports to Manager that is a kind of Employee(1) that works for Company
35
+ Employee(1) reports to Manager that is a kind of Employee(2) that works for Company
38
36
  if and only if
39
- Employee(2) works for Company;
37
+ Employee(1) works for Company;
@@ -28,6 +28,6 @@ Service earned a tip of at most one Amount;
28
28
  /*
29
29
  * Constraints:
30
30
  */
31
- Service (where Waiter served Meal) earned a tip of Amount
31
+ Service (in which Waiter served Meal) earned a tip of Amount
32
32
  if and only if
33
33
  Waiter for serving Meal reported a tip of Amount;
@@ -3,7 +3,7 @@ vocabulary Units;
3
3
  /*
4
4
  * Units
5
5
  */
6
- 1 converts to K;
6
+ 1000.0 converts to K;
7
7
  0.000000000000000001 converts to atto;
8
8
  13.0 converts to bakersdozen;
9
9
  1 converts to bit;
data/index.html CHANGED
File without changes
@@ -64,6 +64,8 @@ module ActiveFacts
64
64
 
65
65
  rule joined_clauses
66
66
  qualified_clauses
67
+ # REVISIT: This creates no precedence between and/or, which could cause confusion.
68
+ # Should disallow mixed conjuntions - using a sempred?
67
69
  ftail:( conjunction:(',' / and / or ) s qualified_clauses s )*
68
70
  {
69
71
  def ast
@@ -192,13 +194,13 @@ module ActiveFacts
192
194
  end
193
195
 
194
196
  rule condition_contraction
195
- role p:post_qualifiers? q:qualifier? s comparator s e2:expression
197
+ role pq:post_qualifiers? q:qualifier? s comparator s e2:expression
196
198
  !phrase # The contracted_clauses must not continue here!
197
199
  {
198
200
  def ast
199
201
  c = Compiler::Comparison.new(comparator.text_value, role.ast, e2.ast, q.empty? ? [] : [q.text_value])
200
202
  c.conjunction = comparator.text_value
201
- [ role.ast, p.empty? ? [] : p.list, c ]
203
+ [ role.ast, pq.empty? ? [] : pq.list, c ]
202
204
  end
203
205
  }
204
206
  end
@@ -248,9 +250,36 @@ module ActiveFacts
248
250
  }
249
251
  end
250
252
 
251
- # This is the rule that causes most back-tracking. I think you can see why.
252
253
  rule role
253
- q:(quantifier enforcement)?
254
+ aggregate
255
+ /
256
+ simple_role
257
+ end
258
+
259
+ rule aggregate
260
+ aggregate:id s of s term s # REVISIT: this term may need to pre-scanned in the qualified_clauses
261
+ in s '('
262
+ qualified_clauses # REVISIT: Need to test to verify this is the right level (not joined_clauses, etc)
263
+ s ')'
264
+ {
265
+ def ast
266
+ raise "Not implemented: AST for '#{aggregate.text_value} of #{term.text_value}'"
267
+ # This returns just the role with the nested clauses, which doesn;t even work:
268
+ term.ast(
269
+ nil, # No quantifier
270
+ nil, # No function call
271
+ nil, # No role_name
272
+ nil, # No value_constraint
273
+ nil, # No literal
274
+ qualified_clauses.ast
275
+ )
276
+ end
277
+ }
278
+ end
279
+
280
+ # This is the rule that causes most back-tracking. I think you can see why.
281
+ rule simple_role
282
+ q:(quantifier enforcement cn:context_note?)?
254
283
  player:derived_variable
255
284
  lr:(
256
285
  literal u:unit?
@@ -261,12 +290,16 @@ module ActiveFacts
261
290
  {
262
291
  def ast
263
292
  if !q.empty? && q.quantifier.value
264
- quantifier = Compiler::Quantifier.new(q.quantifier.value[0], q.quantifier.value[1], q.enforcement.ast)
293
+ quantifier = Compiler::Quantifier.new(
294
+ q.quantifier.value[0],
295
+ q.quantifier.value[1],
296
+ q.enforcement.ast,
297
+ q.cn.empty? ? nil : q.cn.ast
298
+ )
265
299
  end
266
300
  if !lr.empty?
267
301
  if lr.respond_to?(:literal)
268
- literal = lr.literal.value
269
- raise "Literals with units are not yet processed" unless lr.u.empty?
302
+ literal = Compiler::Literal.new(lr.literal.value, lr.u.empty? ? nil : lr.u.text_value)
270
303
  end
271
304
  value_constraint = Compiler::ValueConstraint.new(lr.value_constraint.ranges, lr.value_constraint.units, lr.enforcement.ast) if lr.respond_to?(:value_constraint)
272
305
  raise "It is not permitted to provide both a literal value and a value constraint" if value_constraint and literal
@@ -286,7 +319,7 @@ module ActiveFacts
286
319
  end
287
320
 
288
321
  rule objectification_join
289
- '(' s where s facts:joined_clauses s ')' s
322
+ '(' s in_which s facts:joined_clauses s ')' s
290
323
  {
291
324
  def ast
292
325
  facts.ast
@@ -38,6 +38,12 @@ module ActiveFacts
38
38
  { def independent; !i.empty?; end }
39
39
  end
40
40
 
41
+ rule in_which # Introduce an objectification join
42
+ where / # Old syntax
43
+ in s which # preferred syntax
44
+ { def independent; !i.empty?; end }
45
+ end
46
+
41
47
  # Units conversion keyword
42
48
  rule conversion
43
49
  converts s a:(approximately s)? to s
@@ -240,6 +246,7 @@ module ActiveFacts
240
246
  rule feminine 'feminine' !alphanumeric end
241
247
  rule identified ('known'/'identified') !alphanumeric end
242
248
  rule if 'if' !alphanumeric end
249
+ rule in 'in' !alphanumeric end
243
250
  rule import 'import' !alphanumeric end
244
251
  rule independent 'independent' !alphanumeric end
245
252
  rule intransitive 'intransitive' !alphanumeric end
@@ -250,6 +257,7 @@ module ActiveFacts
250
257
  rule maybe 'maybe' !alphanumeric end
251
258
  rule only 'only' !alphanumeric end
252
259
  rule or 'or' !alphanumeric end
260
+ rule of 'of' !alphanumeric end
253
261
  rule ordering_prefix by s (ascending/descending)? s end
254
262
  rule otherwise 'otherwise' !alphanumeric end
255
263
  rule partitioned 'partitioned' !alphanumeric end
@@ -271,6 +279,8 @@ module ActiveFacts
271
279
  rule true 'true' !alphanumeric end
272
280
  rule vocabulary 'vocabulary' !alphanumeric end
273
281
  rule where 'where' !alphanumeric end
282
+ rule which 'which' !alphanumeric end
283
+ rule was 'was' !alphanumeric end
274
284
  rule who 'who' !alphanumeric end
275
285
 
276
286
  end
@@ -164,7 +164,9 @@ module ActiveFacts
164
164
  end
165
165
 
166
166
  rule mapping_pragma
167
- (independent / separate / partitioned / personal / feminine / masculine)
167
+ (independent / separate / partitioned / personal / feminine / masculine /
168
+ was s names:(id s)+ { def text_value; [ was.text_value, names.elements.map{|n|n.text_value} ]; end }
169
+ )
168
170
  { def value; text_value; end }
169
171
  end
170
172
 
@@ -8,19 +8,8 @@ module ActiveFacts
8
8
  module CQL
9
9
  grammar Terms
10
10
  rule term_definition_name
11
- id s
12
- t:(!non_term_def id s)*
13
- {
14
- def value
15
- t.elements.inject([
16
- id.value
17
- ]){|a, e| a << e.id.value}*' '
18
- end
19
-
20
- def node_type
21
- :term
22
- end
23
- }
11
+ id s t:(!non_term_def id s)*
12
+ <Parser::TermDefinitionNameNode>
24
13
  end
25
14
 
26
15
  rule non_term_def
@@ -69,12 +58,15 @@ module ActiveFacts
69
58
  &{|s| input.context.reset_role_names }
70
59
  (
71
60
  context_note # Context notes have different lexical conventions
72
- / '(' as S term_definition_name s ')' s # Prescan for a Role Name
61
+ / '(' as S term_definition_name s ')' s # Prepare for a Role Name
73
62
  &{|s| input.context.role_name(s[3].value) }
74
- / new_derived_value
75
- / new_adjective_term # Adjective definitions
63
+ / new_derived_value # Prepare for a derived term
64
+ / new_adjective_term # Prepae for an existing term with new Adjectives
65
+ # The remaining rules exist to correctly eat up anything that doesn't match the above:
76
66
  / global_term # If we see A B - C D, don't recognise B as a new adjective for C D.
67
+ / prescan_aggregate
77
68
  / id
69
+ # / literal # REVISIT: Literals might contain "(as Foo)" and mess things up
78
70
  / range # Covers all numbers and strings
79
71
  / comparator # handle two-character operators
80
72
  / S # White space and comments, must precede / and *
@@ -82,13 +74,9 @@ module ActiveFacts
82
74
  )* [?;] s
83
75
  end
84
76
 
85
- rule derived_value_continuation
86
- s '-' tail:(s !global_term !(that/who) id)*
87
- {
88
- def value
89
- tail.elements.map{|e| e.id.text_value}
90
- end
91
- }
77
+ # Not sure this is even needed, but it doesn't seem to hurt:
78
+ rule prescan_aggregate
79
+ aggregate_type:id s of s global_term in s &'('
92
80
  end
93
81
 
94
82
  rule new_derived_value
@@ -105,12 +93,24 @@ module ActiveFacts
105
93
  }
106
94
  end
107
95
 
96
+ # Derived values are new terms introduced by an = sign before an expression
97
+ # This rule handles trailing words of a multi-word derived value
98
+ rule derived_value_continuation
99
+ s '-' tail:(s !global_term !(that/who) id)*
100
+ {
101
+ def value
102
+ tail.elements.map{|e| e.id.text_value}
103
+ end
104
+ }
105
+ end
106
+
107
+ # Used during the pre-scan, match a term with new adjective(s)
108
108
  rule new_adjective_term
109
- !global_term adj:id '-' lead_intervening s global_term # Definitely a new leading adjective for this term
110
- &{|s| input.context.new_leading_adjective_term([s[1].text_value, s[3].value].compact*" ", s[5].text_value) }
109
+ !global_term adj:id '-' '-'? lead_intervening s global_term # Definitely a new leading adjective for this term
110
+ &{|s| adj = [s[1].text_value, s[4].value].compact*" "; input.context.new_leading_adjective_term(adj, s[6].text_value) }
111
111
  /
112
- global_term s trail_intervening '-' !global_term adj:id # Definitely a new trailing adjective for this term
113
- &{|s| input.context.new_trailing_adjective_term([s[2].value, s[5].text_value].compact*" ", s[0].text_value) }
112
+ global_term s trail_intervening '-' '-'? !global_term adj:id # Definitely a new trailing adjective for this term
113
+ &{|s| adj = [s[2].value, s[6].text_value].compact*" "; input.context.new_trailing_adjective_term(adj, s[0].text_value) }
114
114
  end
115
115
 
116
116
  rule lead_intervening # Words intervening between a new adjective and the term
@@ -133,33 +133,14 @@ module ActiveFacts
133
133
 
134
134
  # This is the rule to use after the prescan; it only succeeds on a complete term or role reference
135
135
  rule term
136
- s head:id x &{|s| w = s[1].text_value; input.context.term_starts?(w, s[2]) }
137
- tail:(s '-'? s w:id &{|s| w = s[3].text_value; input.context.term_continues?(w) })*
138
- &{|s| input.context.term_complete? }
139
- {
140
- def ast quantifier = nil, function_call = nil, role_name = nil, value_constraint = nil, literal = nil, nested_clauses = nil
141
- t = x.context[:term]
142
- gt = x.context[:global_term]
143
- leading_adjective = t[0...-gt.size-1] if t.size > gt.size and t[-gt.size..-1] == gt
144
- trailing_adjective = t[gt.size+1..-1] if t.size > gt.size and t[0...gt.size] == gt
145
- Compiler::VarRef.new(gt, leading_adjective, trailing_adjective, quantifier, function_call, role_name, value_constraint, literal, nested_clauses)
146
- end
147
-
148
- def value # Sometimes we just want the full term name
149
- x.context[:term]
150
- end
151
- def node_type; :term; end
152
- }
136
+ s head:id x &{|s| w = s[1].text_value; input.context.term_starts?(w, s[2]) }
137
+ tail:(
138
+ s '-'? dbl:'-'? s w:id &{|s| w = s[4].text_value; input.context.term_continues?(w) }
139
+ )* &{|s| input.context.term_complete? }
140
+ <Parser::TermNode>
153
141
  /
154
- s head:id '-' s term &{|s| s[4].ast.leading_adjective == nil }
155
- {
156
- def ast quantifier = nil, function_call = nil, role_name = nil, value_constraint = nil, literal = nil, nested_clauses = nil
157
- ast = term.ast(quantifier, function_call, role_name, value_constraint, literal, nested_clauses)
158
- ast.leading_adjective = head.text_value
159
- ast
160
- end
161
- def node_type; :term; end
162
- }
142
+ s head:id '-' '-'? s term &{|s| s[5].ast.leading_adjective == nil }
143
+ <Parser::TermLANode>
163
144
  end
164
145
 
165
146
  rule x
@@ -72,7 +72,7 @@ module ActiveFacts
72
72
 
73
73
  def unit? s
74
74
  name = @constellation.Name[s]
75
- units = !name ? [] : name.all_unit.to_a + name.all_unit_as_plural_name.to_a
75
+ units = (!name ? [] : Array(name.unit) + Array(name.unit_as_plural_name)).uniq
76
76
  debug :units, "Looking for unit #{s}, got #{units.map{|u|u.name}.inspect}"
77
77
  units.size > 0
78
78
  end
@@ -351,13 +351,15 @@ module ActiveFacts
351
351
  if la = role_ref.leading_adjective and !la.empty?
352
352
  # The leading adjectives must match, one way or another
353
353
  la = la.split(/\s+/)
354
- return nil unless la[0,intervening_words.size] == intervening_words
354
+ # We may have hyphenated adjectives. Break them up to check:
355
+ iw = intervening_words.map{|w| w.split(/-/)}.flatten
356
+ return nil unless la[0,iw.size] == iw
355
357
  # Any intervening_words matched, see what remains
356
- la.slice!(0, intervening_words.size)
358
+ la.slice!(0, iw.size)
357
359
 
358
360
  # If there were intervening_words, the remaining reading adjectives must match the phrase's leading_adjective exactly.
359
361
  phrase_la = (next_player_phrase.leading_adjective||'').split(/\s+/)
360
- return nil if !intervening_words.empty? && la != phrase_la
362
+ return nil if !iw.empty? && la != phrase_la
361
363
  # If not, the phrase's leading_adjectives must *end* with the reading's
362
364
  return nil if phrase_la[-la.size..-1] != la
363
365
  role_has_residual_adjectives = true if phrase_la.size > la.size
@@ -468,7 +470,8 @@ module ActiveFacts
468
470
  # the role_ref, those must be local, and we'll need to extract them.
469
471
 
470
472
  if rra = side_effect.role_ref.trailing_adjective
471
- debug :matching, "Deleting matched trailing adjective '#{rra}'#{side_effect.absorbed_followers>0 ? " in #{side_effect.absorbed_followers} followers" : ""}"
473
+ debug :matching, "Deleting matched trailing adjective '#{rra}'#{side_effect.absorbed_followers>0 ? " in #{side_effect.absorbed_followers} followers" : ""}, cost is #{side_effect.cost}"
474
+ side_effect.cancel_cost side_effect.absorbed_followers
472
475
 
473
476
  # These adjective(s) matched either an adjective here, or a follower word, or both.
474
477
  if a = phrase.trailing_adjective
@@ -487,7 +490,8 @@ module ActiveFacts
487
490
  end
488
491
 
489
492
  if rra = side_effect.role_ref.leading_adjective
490
- debug :matching, "Deleting matched leading adjective '#{rra}'#{side_effect.absorbed_precursors>0 ? " in #{side_effect.absorbed_precursors} precursors" : ""}}"
493
+ debug :matching, "Deleting matched leading adjective '#{rra}'#{side_effect.absorbed_precursors>0 ? " in #{side_effect.absorbed_precursors} precursors" : ""}, cost is #{side_effect.cost}"
494
+ side_effect.cancel_cost side_effect.absorbed_precursors
491
495
 
492
496
  # These adjective(s) matched either an adjective here, or a precursor word, or both.
493
497
  if a = phrase.leading_adjective
@@ -694,11 +698,16 @@ module ActiveFacts
694
698
  @absorbed_followers = absorbed_followers
695
699
  @common_supertype = common_supertype
696
700
  @residual_adjectives = residual_adjectives
701
+ @cancelled_cost = 0
697
702
  debug :matching_fails, "Saving side effects for #{@phrase.term}, absorbs #{@absorbed_precursors}/#{@absorbed_followers}#{@common_supertype ? ', join over supertype '+ @common_supertype.name : ''}" if @absorbed_precursors+@absorbed_followers+(@common_supertype ? 1 : 0) > 0
698
703
  end
699
704
 
700
705
  def cost
701
- absorbed_precursors + absorbed_followers + (common_supertype ? 1 : 0)
706
+ absorbed_precursors + absorbed_followers + (common_supertype ? 1 : 0) - @cancelled_cost
707
+ end
708
+
709
+ def cancel_cost c
710
+ @cancelled_cost += c
702
711
  end
703
712
 
704
713
  def to_s
@@ -963,16 +972,20 @@ module ActiveFacts
963
972
 
964
973
  class Quantifier
965
974
  attr_accessor :enforcement
975
+ attr_accessor :context_note
966
976
  attr_reader :min, :max
967
977
 
968
- def initialize min, max, enforcement = nil
978
+ def initialize min, max, enforcement = nil, context_note = nil
969
979
  @min = min
970
980
  @max = max
971
981
  @enforcement = enforcement
982
+ @context_note = context_note
972
983
  end
973
984
 
974
985
  def inspect
975
- "[#{@min}..#{@max}]"
986
+ "[#{@min}..#{@max}]#{
987
+ @context_note && ' ' + @context_note.inspect
988
+ }"
976
989
  end
977
990
  end
978
991