activefacts 0.7.3 → 0.8.5

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 (94) hide show
  1. data/LICENSE +19 -0
  2. data/Manifest.txt +24 -2
  3. data/Rakefile +25 -3
  4. data/bin/afgen +1 -1
  5. data/bin/cql +13 -2
  6. data/css/offline.css +3 -0
  7. data/css/orm2.css +24 -0
  8. data/css/print.css +8 -0
  9. data/css/style-print.css +357 -0
  10. data/css/style.css +387 -0
  11. data/download.html +85 -0
  12. data/examples/CQL/Address.cql +3 -3
  13. data/examples/CQL/Blog.cql +13 -14
  14. data/examples/CQL/CompanyDirectorEmployee.cql +4 -4
  15. data/examples/CQL/Death.cql +3 -2
  16. data/examples/CQL/Genealogy.cql +13 -11
  17. data/examples/CQL/Marriage.cql +2 -2
  18. data/examples/CQL/Metamodel.cql +136 -93
  19. data/examples/CQL/MultiInheritance.cql +2 -2
  20. data/examples/CQL/OilSupply.cql +14 -10
  21. data/examples/CQL/Orienteering.cql +22 -19
  22. data/examples/CQL/PersonPlaysGame.cql +3 -2
  23. data/examples/CQL/SchoolActivities.cql +4 -2
  24. data/examples/CQL/SimplestUnary.cql +1 -1
  25. data/examples/CQL/SubtypePI.cql +6 -7
  26. data/examples/CQL/Warehousing.cql +16 -19
  27. data/examples/CQL/unit.cql +584 -0
  28. data/examples/index.html +276 -0
  29. data/examples/intro.html +497 -0
  30. data/examples/local.css +20 -0
  31. data/index.html +96 -0
  32. data/lib/activefacts/api/concept.rb +48 -46
  33. data/lib/activefacts/api/constellation.rb +43 -23
  34. data/lib/activefacts/api/entity.rb +2 -2
  35. data/lib/activefacts/api/instance.rb +6 -2
  36. data/lib/activefacts/api/instance_index.rb +5 -0
  37. data/lib/activefacts/api/value.rb +8 -2
  38. data/lib/activefacts/api/vocabulary.rb +15 -10
  39. data/lib/activefacts/cql/CQLParser.treetop +109 -88
  40. data/lib/activefacts/cql/Concepts.treetop +32 -10
  41. data/lib/activefacts/cql/Context.treetop +34 -0
  42. data/lib/activefacts/cql/Expressions.treetop +9 -9
  43. data/lib/activefacts/cql/FactTypes.treetop +30 -31
  44. data/lib/activefacts/cql/Language/English.treetop +50 -0
  45. data/lib/activefacts/cql/LexicalRules.treetop +2 -1
  46. data/lib/activefacts/cql/Terms.treetop +117 -0
  47. data/lib/activefacts/cql/ValueTypes.treetop +152 -0
  48. data/lib/activefacts/cql/compiler.rb +1718 -0
  49. data/lib/activefacts/cql/parser.rb +124 -57
  50. data/lib/activefacts/generate/absorption.rb +1 -1
  51. data/lib/activefacts/generate/cql.rb +111 -100
  52. data/lib/activefacts/generate/cql/html.rb +5 -5
  53. data/lib/activefacts/generate/oo.rb +3 -3
  54. data/lib/activefacts/generate/ordered.rb +51 -19
  55. data/lib/activefacts/generate/ruby.rb +10 -8
  56. data/lib/activefacts/generate/sql/mysql.rb +14 -10
  57. data/lib/activefacts/generate/sql/server.rb +29 -24
  58. data/lib/activefacts/input/cql.rb +9 -1264
  59. data/lib/activefacts/input/orm.rb +213 -200
  60. data/lib/activefacts/persistence/columns.rb +11 -10
  61. data/lib/activefacts/persistence/index.rb +15 -18
  62. data/lib/activefacts/persistence/reference.rb +17 -17
  63. data/lib/activefacts/persistence/tables.rb +50 -51
  64. data/lib/activefacts/version.rb +1 -1
  65. data/lib/activefacts/vocabulary/extensions.rb +79 -8
  66. data/lib/activefacts/vocabulary/metamodel.rb +183 -114
  67. data/spec/absorption_ruby_spec.rb +99 -0
  68. data/spec/absorption_spec.rb +3 -4
  69. data/spec/api/constellation.rb +1 -1
  70. data/spec/api/entity_type.rb +3 -1
  71. data/spec/api/instance.rb +4 -2
  72. data/spec/api/roles.rb +8 -6
  73. data/spec/api_spec.rb +1 -2
  74. data/spec/cql/context_spec.rb +71 -0
  75. data/spec/cql/samples_spec.rb +154 -0
  76. data/spec/cql/unit_spec.rb +375 -0
  77. data/spec/cql_cql_spec.rb +31 -21
  78. data/spec/cql_mysql_spec.rb +70 -0
  79. data/spec/cql_parse_spec.rb +15 -9
  80. data/spec/cql_ruby_spec.rb +27 -13
  81. data/spec/cql_sql_spec.rb +42 -16
  82. data/spec/cql_symbol_tables_spec.rb +2 -3
  83. data/spec/cqldump_spec.rb +7 -7
  84. data/spec/helpers/file_matcher.rb +39 -0
  85. data/spec/norma_cql_spec.rb +20 -12
  86. data/spec/norma_ruby_spec.rb +6 -3
  87. data/spec/norma_sql_spec.rb +6 -3
  88. data/spec/norma_tables_spec.rb +6 -4
  89. data/spec/spec_helper.rb +27 -8
  90. data/status.html +69 -0
  91. data/why.html +60 -0
  92. metadata +34 -11
  93. data/lib/activefacts/cql/DataTypes.treetop +0 -81
  94. data/spec/cql_unit_spec.rb +0 -330
@@ -11,80 +11,147 @@ require 'treetop'
11
11
  require 'activefacts/cql/LexicalRules'
12
12
  require 'activefacts/cql/Language/English'
13
13
  require 'activefacts/cql/Expressions'
14
+ require 'activefacts/cql/Terms'
14
15
  require 'activefacts/cql/Concepts'
15
- require 'activefacts/cql/DataTypes'
16
+ require 'activefacts/cql/ValueTypes'
16
17
  require 'activefacts/cql/FactTypes'
18
+ require 'activefacts/cql/Context'
17
19
  require 'activefacts/cql/CQLParser'
18
20
 
19
21
  module ActiveFacts
20
- # Extend the generated parser:
21
- class CQLParser
22
- include ActiveFacts
23
-
24
- # Repeatedly parse rule_name until all input is consumed,
25
- # returning an array of syntax trees for each definition.
26
- def parse_all(input, rule_name = nil, &block)
27
- self.root = rule_name if rule_name
28
-
29
- @index = 0 # Byte offset to start next parse
30
- self.consume_all_input = false
31
- results = []
32
- begin
33
- node = parse(input, :index => @index)
34
- return nil unless node
35
- node = block.call(node) if block
36
- results << node if node
37
- end until self.index == @input_length
38
- results
39
- end
22
+ module CQL
23
+ # Extend the generated parser:
24
+ class Parser < CQLParser
25
+ include ActiveFacts
26
+
27
+ class BlackHole
28
+ def method_missing(m, *p, &b)
29
+ self # Make all calls vanish
30
+ end
31
+ end
32
+
33
+ class InputProxy < Object
34
+ attr_reader :context
35
+
36
+ def initialize(input, context)
37
+ @input = input
38
+ @context = context
39
+ end
40
+
41
+ def length
42
+ @input.length
43
+ end
44
+
45
+ def size
46
+ length
47
+ end
48
+
49
+ def [](*a)
50
+ @input[*a]
51
+ end
52
+
53
+ def index(*a)
54
+ @input.index(*a)
55
+ end
56
+ end
57
+
58
+ def context
59
+ @context ||= BlackHole.new
60
+ end
40
61
 
41
- def definition(node)
42
- name, ast = *node.value
43
- kind, *value = *ast
44
-
45
- begin
46
- debug "CQL: Processing definition #{[kind, name].compact*" "}" do
47
- case kind
48
- when :vocabulary
49
- [kind, name]
50
- when :data_type
51
- data_type(name, value)
52
- when :entity_type
53
- supertypes = value.shift
54
- entity_type(name, supertypes, value)
55
- when :fact_type
56
- f = fact_type(name, value)
57
- when :constraint
58
- ast
62
+ def parse(input, options = {})
63
+ input = InputProxy.new(input, context) unless input.respond_to?(:context)
64
+ super(input, options)
65
+ end
66
+
67
+ # Repeatedly parse rule_name until all input is consumed,
68
+ # returning an array of syntax trees for each definition.
69
+ def parse_all(input, rule_name = nil, &block)
70
+ self.root = rule_name if rule_name
71
+
72
+ @index = 0 # Byte offset to start next parse
73
+ self.consume_all_input = false
74
+ results = []
75
+ begin
76
+ node = parse(InputProxy.new(input, context), :index => @index)
77
+ return nil unless node
78
+ node = block.call(node) if block
79
+ results << node if node
80
+ end until self.index == @input_length
81
+ results
82
+ end
83
+
84
+ def definition(node)
85
+ name, ast = *node.value
86
+ kind, *value = *ast
87
+
88
+ begin
89
+ debug "CQL: Processing definition #{[kind, name].compact*" "}" do
90
+ case kind
91
+ when :vocabulary
92
+ [kind, name]
93
+ when :value_type
94
+ value_type_ast(name, value)
95
+ when :entity_type
96
+ supertypes = value.shift
97
+ entity_type_ast(name, supertypes, value)
98
+ when :fact_type
99
+ f = fact_type_ast(name, value)
100
+ when :unit
101
+ ast
102
+ when :constraint
103
+ ast
104
+ else
105
+ raise "CQL: internal error, unknown definition kind"
106
+ end
59
107
  end
60
108
  end
109
+ rescue => e
110
+ raise "in #{kind.to_s.camelcase(true)} definition, #{e.message}:\n\t#{node.text_value}" +
111
+ (ENV['DEBUG'] =~ /\bexception\b/ ? "\nfrom\t"+e.backtrace*"\n\t" : "")
61
112
  end
62
- rescue => e
63
- raise "in #{kind.to_s.camelcase(true)} definition, #{e.message}:\n\t#{node.text_value}"
64
- end
65
113
 
66
- def data_type(name, value)
67
- # REVISIT: Massage/check data type here?
68
- [:data_type, name, *value]
69
- end
114
+ def value_type_ast(name, value)
115
+ # REVISIT: Massage/check value type here?
116
+ [:value_type, name, *value]
117
+ end
70
118
 
71
- def entity_type(name, supertypes, value)
72
- #print "entity_type parameters for #{name}: "; p value
73
- identification, clauses = *value
74
- clauses ||= []
119
+ def entity_type_ast(name, supertypes, value)
120
+ #print "entity_type parameters for #{name}: "; p value
121
+ identification, mapping_pragmas, clauses = *value
122
+ clauses ||= []
75
123
 
76
- # raise "Entity type clauses must all be fact types" if clauses.detect{|c| c[0] != :fact_clause }
124
+ # raise "Entity type clauses must all be fact types" if clauses.detect{|c| c[0] != :fact_clause }
77
125
 
78
- [:entity_type, name, supertypes, identification, clauses]
79
- end
126
+ [:entity_type, name, supertypes, identification, mapping_pragmas, clauses]
127
+ end
128
+
129
+ def fact_type_ast(name, value)
130
+ clauses, conditions = value
131
+
132
+ if conditions.empty? && includes_literals(clauses)
133
+ [:fact, nil, clauses]
134
+ elsif clauses.size == 1 &&
135
+ (popname = clauses[0][2]).size == 1 &&
136
+ popname[0].keys == [:word] &&
137
+ includes_literals(conditions)
138
+ [:fact, popname[0][:word], conditions]
139
+ else
140
+ [:fact_type, name, clauses, conditions]
141
+ end
142
+ end
80
143
 
81
- def fact_type(name, value)
82
- defined_readings, *clauses = value
144
+ def includes_literals(clauses)
145
+ clauses.detect do |clause|
146
+ raise "alternate clauses are not yet supported" if clause[0] == :"||"
147
+ fc, qualifiers, phrases, context_note = *clause
148
+ phrases.detect{|w| w[:literal]}
149
+ end
150
+ end
83
151
 
84
- [:fact_type, name, defined_readings, clauses]
85
152
  end
86
153
 
87
154
  end
88
155
 
89
- Polyglot.register('cql', CQLParser)
156
+ Polyglot.register('cql', CQL::Parser)
90
157
  end
@@ -34,7 +34,7 @@ module ActiveFacts
34
34
  multi_absorption_vts = 0
35
35
  multi_absorption_ets = 0
36
36
  @vocabulary.tables
37
- @vocabulary.all_feature.sort_by{|c| c.name}.each do |o|
37
+ @vocabulary.all_concept.sort_by{|c| c.name}.each do |o|
38
38
  next if !o.is_table
39
39
  show(o)
40
40
  end
@@ -31,33 +31,22 @@ module ActiveFacts
31
31
 
32
32
  def value_type_dump(o)
33
33
  return unless o.supertype # An imported type
34
+
35
+ # REVISIT: A ValueType that is only used as a reference mode need not be emitted here. We haven't detected this situation yet however...
36
+
34
37
  if o.name == o.supertype.name
35
- # In ActiveFacts, parameterising a ValueType will create a new datatype
36
- # throw Can't handle parameterized value type of same name as its datatype" if ...
38
+ # In ActiveFacts, parameterising a ValueType will create a new ValueType
39
+ # throw Can't handle parameterized value type of same name as its ValueType" if ...
37
40
  end
38
41
 
39
42
  parameters =
40
43
  [ o.length != 0 || o.scale != 0 ? o.length : nil,
41
44
  o.scale != 0 ? o.scale : nil
42
45
  ].compact
43
- parameters = parameters.length > 0 ? "("+parameters.join(",")+")" : "()"
44
-
45
- #" restricted to {#{(allowed_values.map{|r| r.inspect}*", ").gsub('"',"'")}}")
46
+ parameters = parameters.length > 0 ? "("+parameters.join(",")+")" : ""
46
47
 
47
48
  puts "#{o.name} is written as #{o.supertype.name}#{ parameters }#{
48
- o.value_restriction ? " restricted to {#{
49
- o.value_restriction.all_allowed_range.sort_by{|ar|
50
- ((min = ar.value_range.minimum_bound) && min.value) ||
51
- ((max = ar.value_range.maximum_bound) && max.value)
52
- }.map{|ar|
53
- # REVISIT: Need to display as string or numeric according to type here...
54
- min = ar.value_range.minimum_bound
55
- max = ar.value_range.maximum_bound
56
-
57
- (min ? min.value : "") +
58
- (min.value != (max&&max.value) ? (".." + (max ? max.value : "")) : "")
59
- }*", "
60
- }}" : ""
49
+ o.value_restriction && " "+o.value_restriction.describe
61
50
  };"
62
51
  end
63
52
 
@@ -65,6 +54,18 @@ module ActiveFacts
65
54
  reading << " [#{(ring.ring_type.scan(/[A-Z][a-z]*/)*", ").downcase}]"
66
55
  end
67
56
 
57
+ def mapping_pragma(entity_type)
58
+ ti = entity_type.all_type_inheritance_as_subtype
59
+ assimilation = ti.map{|t| t.assimilation }.compact[0]
60
+ return "" unless entity_type.is_independent || assimilation
61
+ " [" +
62
+ [
63
+ entity_type.is_independent ? "independent" : nil,
64
+ assimilation || nil
65
+ ].compact*", " +
66
+ "]"
67
+ end
68
+
68
69
  def identified_by_roles_and_facts(entity_type, identifying_roles, identifying_facts, preferred_readings)
69
70
  identifying_role_names = identifying_roles.map{|role|
70
71
  preferred_role_ref = preferred_readings[role.fact_type].role_sequence.all_role_ref.detect{|reading_rr|
@@ -94,13 +95,16 @@ module ActiveFacts
94
95
  # Just beware that readings having the same players will be considered to be of the same fact type, even if they're not.
95
96
 
96
97
  # Detect standard reference-mode scenarios
97
- ft = identifying_facts[0]
98
+ external_identifying_facts = identifying_facts - [entity_type.fact_type]
99
+ ft = external_identifying_facts[0]
98
100
  fact_constraints = nil
99
- ftr = ft.all_role.sort_by{|role| role.ordinal}
100
- if identifying_facts.size == 1 and
101
+ ftr = ft && ft.all_role.sort_by{|role| role.ordinal}
102
+ if external_identifying_facts.size == 1 and
101
103
  entity_role = ftr[n = (ftr[0].concept == entity_type ? 0 : 1)] and
102
104
  value_role = ftr[1-n] and
103
- value_name = value_role.concept.name and
105
+ value_player = value_role.concept and
106
+ value_player.is_a?(ActiveFacts::Metamodel::ValueType) and
107
+ value_name = value_player.name and
104
108
  residual = value_name.gsub(%r{#{entity_role.concept.name}},'') and
105
109
  residual != '' and
106
110
  residual != value_name
@@ -111,13 +115,13 @@ module ActiveFacts
111
115
  # Detect standard reference-mode readings:
112
116
  forward_reading = reverse_reading = nil
113
117
  ft.all_reading.each do |reading|
114
- if reading.reading_text =~ /^\{(\d)\} has \{\d\}$/
118
+ if reading.text =~ /^\{(\d)\} has \{\d\}$/
115
119
  if reading.role_sequence.all_role_ref.detect{|rr| rr.ordinal == $1.to_i}.role == entity_role
116
120
  forward_reading = reading
117
121
  else
118
122
  reverse_reading = reading
119
123
  end
120
- elsif reading.reading_text =~ /^\{(\d)\} is of \{\d\}$/
124
+ elsif reading.text =~ /^\{(\d)\} is of \{\d\}$/
121
125
  if reading.role_sequence.all_role_ref.detect{|rr| rr.ordinal == $1.to_i}.role == value_role
122
126
  reverse_reading = reading
123
127
  else
@@ -141,6 +145,7 @@ module ActiveFacts
141
145
  @constraints_used[pc] = true
142
146
  end
143
147
  end
148
+ fact_constraints += Array(@presence_constraints_by_fact[entity_type.fact_type])
144
149
 
145
150
  @fact_types_dumped[ft] = true
146
151
 
@@ -148,10 +153,19 @@ module ActiveFacts
148
153
  other_readings = ft.all_reading - [forward_reading] - [reverse_reading]
149
154
  debug :mode, "--- other_readings.size now = #{other_readings.size}" if other_readings.size > 0
150
155
 
151
- fact_text = other_readings.map do |reading|
152
- expanded_reading(reading, fact_constraints, true)
153
- end*",\n\t"
154
- return " identified by its #{residual}" +
156
+ fact_text = (
157
+ other_readings.map do |reading|
158
+ expanded_reading(reading, fact_constraints, true)
159
+ end +
160
+ (entity_type.fact_type ?
161
+ fact_readings_with_constraints(entity_type.fact_type, fact_constraints) : []
162
+ )
163
+ )*",\n\t"
164
+
165
+ restriction = value_role.role_value_restriction || value_player.value_restriction
166
+ # REVISIT: If both restrictions apply and differ, we can't use a reference mode
167
+ restriction_text = restriction ? " "+restriction.describe : ""
168
+ return " identified by its #{residual}#{restriction_text}#{mapping_pragma(entity_type)}" +
155
169
  (fact_text != "" ? " where\n\t" + fact_text : "")
156
170
  end
157
171
  end
@@ -163,6 +177,7 @@ module ActiveFacts
163
177
  }.flatten*",\n\t"
164
178
 
165
179
  " identified by #{ identifying_role_names*" and " }" +
180
+ mapping_pragma(entity_type) +
166
181
  " where\n\t"+@identifying_fact_text
167
182
  end
168
183
 
@@ -183,7 +198,10 @@ module ActiveFacts
183
198
  print "#{o.name} is a kind of #{ o.supertypes.map(&:name)*", " }"
184
199
  if pi
185
200
  print identified_by(o, pi)
201
+ else
202
+ print mapping_pragma(o)
186
203
  end
204
+
187
205
  # If there's a preferred_identifier for this subtype, identifying readings were emitted
188
206
  print((pi ? "," : " where") + "\n\t" + fact_readings(o.fact_type)) if o.fact_type
189
207
  puts ";\n"
@@ -191,7 +209,7 @@ module ActiveFacts
191
209
 
192
210
  def non_subtype_dump(o, pi)
193
211
  print "#{o.name} is" + identified_by(o, pi)
194
- print(" where\n\t"+ fact_readings(o.fact_type)) if o.fact_type
212
+ # print(" where\n\t"+ fact_readings(o.fact_type)) if o.fact_type
195
213
  puts ";\n"
196
214
  end
197
215
 
@@ -209,6 +227,7 @@ module ActiveFacts
209
227
  pi = fact_type.entity_type.preferred_identifier
210
228
  if pi && primary_supertype && primary_supertype.preferred_identifier != pi
211
229
  print identified_by(o, pi)
230
+ # REVISIT: This *has* to be wrong. When you fix it, remember mapping_pragmas!
212
231
  print ";\n"
213
232
  end
214
233
  end
@@ -244,55 +263,25 @@ module ActiveFacts
244
263
  end
245
264
 
246
265
  def dump_presence_constraint(c)
247
- roles = c.role_sequence.all_role_ref.map{|rr| rr.role }
248
-
249
- # REVISIT: If only one role is covered and it's mandatory >=1 constraint, use SOME/THAT form:
250
- # each Bug SOME Tester logged THAT Bug;
251
- players = c.role_sequence.all_role_ref.map{|rr| rr.role.concept.name}.uniq
252
-
253
- fact_types = c.role_sequence.all_role_ref.map{|rr| rr.role.fact_type}.uniq
254
- puts \
255
- "each #{players.size > 1 ? "combination " : ""}#{players*", "} occurs #{c.frequency} time in\n\t"+
256
- "#{fact_types.map{|ft| ft.default_reading([], nil)}*",\n\t"}" +
257
- ";"
258
-
259
- =begin
260
- # More than one fact type involved, an external constraint.
261
- fact_type = rr.role.fact_type
262
- # or all facts are binary and the counterparts of the roles are.
263
- puts "// REVISIT: " +
264
- if (player = roleplayer_subclass(roles))
265
- "#{player.name} must play #{c.frequency} of "
266
- else
267
- counterparts = roles.map{|r|
268
- r.fact_type.all_role[r.fact_type.all_role[0] != r ? 0 : -1]
269
- }
270
- player = roleplayer_subclass(counterparts)
271
- "#{c.frequency} #{player ? player.name : "UNKNOWN" } exists for each "
272
- end +
273
- "#{
274
- c.role_sequence.all_role_ref.map{|rr|
275
- "'#{rr.role.fact_type.default_reading([], nil)}'"
276
- }*", "
277
- }"
278
- =end
279
-
280
- =begin
281
- puts \
282
- "FOR each #{players*", "}" +
283
- (c.role_sequence.all_role_ref.size > 1 ? " "+c.frequency+" of these holds" : "") + "\n\t"+
284
- "#{c.role_sequence.all_role_ref.map{|rr|
285
- role = rr.role
286
- fact_type = role.fact_type
287
- some_that = Array.new(fact_type.all_role.size, "some")
288
- c.role_sequence.all_role_ref.each{|rr2|
289
- next if rr2.role.fact_type != fact_type
290
- some_that[fact_type.all_role.index(role)] = "that"
291
- }
292
- rr.role.fact_type.default_reading(some_that, nil)
293
- }*",\n\t"}" +
294
- ";"
295
- =end
266
+ if c.min_frequency == 1 && c.max_frequency == nil and c.role_sequence.all_role_ref.size == 2
267
+ # REVISIT: Implement the "either... or" syntax for a simple external mandatory constraint
268
+ puts \
269
+ "either #{
270
+ c.role_sequence.all_role_ref.map { |rr|
271
+ rr.role.fact_type.default_reading([], nil)
272
+ }*" or "
273
+ };"
274
+ else
275
+ # REVISIT: If only one role is covered and it's mandatory >=1 constraint, use SOME/THAT form:
276
+ # for each Bug SOME Tester logged THAT Bug;
277
+ roles = c.role_sequence.all_role_ref.map{|rr| rr.role }
278
+ players = c.role_sequence.all_role_ref.map{|rr| rr.role.concept.name}.uniq
279
+ fact_types = c.role_sequence.all_role_ref.map{|rr| rr.role.fact_type}.uniq
280
+ puts \
281
+ "each #{players.size > 1 ? "combination " : ""}#{players*", "} occurs #{c.frequency} time in\n\t"+
282
+ "#{fact_types.map{|ft| ft.default_reading([], nil)}*",\n\t"}" +
283
+ ";"
284
+ end
296
285
  end
297
286
 
298
287
  # Find the common supertype of these concepts.
@@ -331,11 +320,13 @@ module ActiveFacts
331
320
  # puts "#{c.class.basename} has #{role_seq_count} scr's: #{scrs.map{|scr| "("+scr.role_sequence.all_role_ref.map{|rr| rr.role.concept.name}*", "+")"}*", "}"
332
321
 
333
322
  players_differ = [] # Record which players are also played by subclasses
334
- players = (0...player_count).map do |pi|
335
- # Find the common supertype of the players of the pi'th role in each sequence
336
- concepts = scrs.map{|r| r.role_sequence.all_role_ref.sort_by{|rr| rr.ordinal}[pi].role.concept }
337
- player, players_differ[pi] = common_supertype(concepts)
338
- raise "Role sequences of #{c.class.basename} must have concepts matching #{concepts.map(&:name)*","} in position #{pi}" unless player
323
+ players = (0...player_count).map do |pindex|
324
+ # Find the common supertype of the players of the pindex'th role in each sequence
325
+ concepts = scrs.map do |r|
326
+ r.role_sequence.all_role_ref.sort_by{|rr| rr.ordinal}[pindex].role.concept
327
+ end
328
+ player, players_differ[pindex] = common_supertype(concepts)
329
+ raise "Role sequences of #{c.class.basename} must have concepts matching #{concepts.map(&:name)*","} in position #{pindex}" unless player
339
330
  player
340
331
  end
341
332
  #puts "#{c.class.basename} has players #{players.map{|p| p.name}*", "}"
@@ -349,22 +340,42 @@ module ActiveFacts
349
340
  return
350
341
  end
351
342
 
352
- mode = c.is_mandatory ? "exactly one" : "at most one"
353
- puts "for each #{players.map{|p| p.name}*", "} #{mode} of these holds:\n\t" +
354
- (scrs.map do |scr|
355
- constrained_roles = scr.role_sequence.all_role_ref.map{|rr| rr.role }
356
- fact_types = constrained_roles.map{|r| r.fact_type }.uniq
357
-
358
- fact_types.map do |fact_type|
359
- # REVISIT: future: Use "THAT" and "SOME" only when:
360
- # - the role player occurs twice in the reading, or
361
- # - is a subclass of the constrained concept, or
362
- reading = fact_type.preferred_reading
363
- expand_constrained(reading, constrained_roles, players, players_differ)
364
- end * " and "
365
-
366
- end*",\n\t"
367
- )+';'
343
+ if scrs.size == 2 && c.is_mandatory
344
+ puts "either " +
345
+ ( scrs.map do |scr|
346
+ constrained_roles = scr.role_sequence.all_role_ref.map{|rr| rr.role }
347
+ fact_types = constrained_roles.map{|r| r.fact_type }.uniq
348
+
349
+ fact_types.map do |fact_type|
350
+ # Choose a reading that starts with the input role (constrained role if none)
351
+ reading = fact_type.all_reading.sort_by{|r| r.ordinal}.detect do |r|
352
+ first_reading_role = r.role_sequence.all_role_ref.detect{|rr| rr.ordinal == 0}.role
353
+ constrained_roles.include?(first_reading_role)
354
+ end
355
+ reading ||= fact_type.preferred_reading
356
+ expand_constrained(reading, constrained_roles, players, players_differ)
357
+ end * " and "
358
+ end*" or "
359
+ ) +
360
+ " but not both;"
361
+ else
362
+ mode = c.is_mandatory ? "exactly one" : "at most one"
363
+ puts "for each #{players.map{|p| p.name}*", "} #{mode} of these holds:\n\t" +
364
+ (scrs.map do |scr|
365
+ constrained_roles = scr.role_sequence.all_role_ref.map{|rr| rr.role }
366
+ fact_types = constrained_roles.map{|r| r.fact_type }.uniq
367
+
368
+ fact_types.map do |fact_type|
369
+ # REVISIT: future: Use "THAT" and "SOME" only when:
370
+ # - the role player occurs twice in the reading, or
371
+ # - is a subclass of the constrained concept, or
372
+ reading = fact_type.preferred_reading
373
+ expand_constrained(reading, constrained_roles, players, players_differ)
374
+ end * " and "
375
+
376
+ end*",\n\t"
377
+ )+';'
378
+ end
368
379
  end
369
380
 
370
381
  # Expand this reading using (in)definite articles where needed
@@ -386,7 +397,7 @@ module ActiveFacts
386
397
  }
387
398
  frequency_constraints = [] unless frequency_constraints.detect{|fc| fc[0] != "some" }
388
399
 
389
- #$stderr.puts "fact_type roles (#{fact_type.all_role.map{|r| r.concept.name}*","}) default_reading '#{fact_type.preferred_reading.reading_text}' roles (#{fact_type.preferred_reading.role_sequence.all_role_ref.map{|rr| rr.role.concept.name}*","}) #{frequency_constraints.inspect}"
400
+ #$stderr.puts "fact_type roles (#{fact_type.all_role.map{|r| r.concept.name}*","}) default_reading '#{fact_type.preferred_reading.text}' roles (#{fact_type.preferred_reading.role_sequence.all_role_ref.map{|rr| rr.role.concept.name}*","}) #{frequency_constraints.inspect}"
390
401
 
391
402
  # REVISIT: Make sure that we refer to the constrained players by their common supertype
392
403