Class: ActiveFacts::Metamodel::Verbaliser

Inherits:
Object
  • Object
show all
Defined in:
lib/activefacts/vocabulary/verbaliser.rb

Overview

The Verbaliser fulfils two roles:

  • Maintains verbalisation context to expand readings using subscripting where needed

  • Verbalises Joins by iteratively choosing a Join Step and expanding readings

The verbalisation context consists of a set of Players, each for one Concept. There may be more than one Player for the same Concept. If adjectives or role names don’t make such duplicates unambiguous, subscripts will be generated. Thus, the verbalisation context must be completely populated before subscript generation, which must be before any Player name gets verbalised.

When a Player occurs in a Join, it corresponds to one Join Node of that Join. Each such Player has one or more JoinRoles, which refer to roles played by that Concept. Where a join traverses two roles of a ternary fact type, there will be a residual node that has only a single JoinRole with no other meaning. A JoinRole must be for exactly one Player, so is used to identify a Player.

When a Player occurs outside a Join, it’s identified by a projected RoleRef. REVISIT: This is untrue when a uniqueness constraint is imported from NORMA. In this case no join will be constructed to project the roles of the constrained object type (only the constrained roles will be projected) - this will be fixed.

Each constraint (except Ring Constraints) has one or more RoleSequence containing the projected RoleRefs. Each constrained RoleSequence may have an associated Join. If it has a Join, each RoleRef is projected from a JoinRole, otherwise none are.

The only type of join possible in a Ring Constraint is a subtyping join, which is always implicit and unambiguous, so is never instantiated.

A constrained RoleSequence that has no explicit Join may have an implicit join, as per ORM2, when the roles aren’t in the same fact type. These implicit joins are over only one Concept, by traversing a single FactType (and possibly, multiple TypeInheritance FactTypes) for each RoleRef. Note however that when the Concept is an objectified Fact Type, the FactType traversed might be a phantom of the objectification. In the case of implicit joins, each Player is identified by the projected RoleRef, except for the joined-over Concept whose Player is… well, read the next paragraph!

REVISIT: I believe that the foregoing paragraph is out of date, except with respect to PresenceConstraints imported from NORMA (both external mandatory and external uniqueness constraints). The joined-over Player in a UC is identified by its RoleRefs in the RoleSequence of the Fact Type’s preferred reading. Subtyping joins in a mandatory constraint will probably malfunction. However, all other such joins are expliciti, and these should be also.

For a SetComparisonConstraint, there are two or more constrained RoleSequences. The matching RoleRefs (by Ordinal position) are for joined players, that is, one individual instance plays both roles. The RoleRefs must (now) be for the same Concept (no implicit subtyping Join is allowed). Instead, the input modules find the closest common supertype and create explicit JoinSteps so its roles can be projected.

When expanding Reading text however, the RoleRefs in the reading’s RoleSequence may be expected not to be attached to the Players for that reading. Instead, the set of one or more RoleRefs which caused that reading to be expanded must be passed in, and the corresponding roles matched with Players to determine the need to emit a subscript.

Defined Under Namespace

Classes: Player

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(role_refs = nil) ⇒ Verbaliser

Returns a new instance of Verbaliser.



83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/activefacts/vocabulary/verbaliser.rb', line 83

def initialize role_refs = nil
  @role_refs = role_refs

  # Verbalisation context:
  @players = []
  @player_by_join_role = {}
  @player_by_role_ref = {}
  @player_joined_over = nil

  # Join Verbaliser context:
  @join = nil
  @join_nodes = []
  @join_steps = []
  @join_steps_by_join_node = {}

  add_role_refs role_refs if role_refs
end

Instance Attribute Details

#joinObject (readonly)

Join Verbaliser context:



78
79
80
# File 'lib/activefacts/vocabulary/verbaliser.rb', line 78

def join
  @join
end

#join_nodesObject (readonly)

All Join Nodes



79
80
81
# File 'lib/activefacts/vocabulary/verbaliser.rb', line 79

def join_nodes
  @join_nodes
end

#join_stepsObject (readonly)

All remaining unemitted Join Steps



80
81
82
# File 'lib/activefacts/vocabulary/verbaliser.rb', line 80

def join_steps
  @join_steps
end

#join_steps_by_join_nodeObject (readonly)

A Hash by Join Node containing an array of remaining steps



81
82
83
# File 'lib/activefacts/vocabulary/verbaliser.rb', line 81

def join_steps_by_join_node
  @join_steps_by_join_node
end

#player_by_join_roleObject (readonly)

Used for each join



70
71
72
# File 'lib/activefacts/vocabulary/verbaliser.rb', line 70

def player_by_join_role
  @player_by_join_role
end

#player_by_role_refObject (readonly)

Used when a constrained role sequence has no join



72
73
74
# File 'lib/activefacts/vocabulary/verbaliser.rb', line 72

def player_by_role_ref
  @player_by_role_ref
end

#player_joined_overObject (readonly)

Used when there’s an implicit join



71
72
73
# File 'lib/activefacts/vocabulary/verbaliser.rb', line 71

def player_joined_over
  @player_joined_over
end

#playersObject (readonly)

Verbalisation context:



69
70
71
# File 'lib/activefacts/vocabulary/verbaliser.rb', line 69

def players
  @players
end

#role_refsObject (readonly)

The projected role references over which we’re verbalising



75
76
77
# File 'lib/activefacts/vocabulary/verbaliser.rb', line 75

def role_refs
  @role_refs
end

Instance Method Details

#add_join_role(player, join_role) ⇒ Object



141
142
143
144
145
146
147
148
149
150
# File 'lib/activefacts/vocabulary/verbaliser.rb', line 141

def add_join_role player, join_role
  return if player.join_roles.include?(join_role)
  jn = join_role.join_node
  if jn1 = player.join_nodes_by_join[jn.join] and jn1 != jn
    raise "Player for #{player.concept.name} may only have one join node per join, not #{jn1.concept.name} and #{jn.concept.name}"
  end
  player.join_nodes_by_join[jn.join] = jn
  @player_by_join_role[join_role] = player
  player.join_roles << join_role
end

#add_role_player(player, role_ref) ⇒ Object

Add a RoleRef to an existing Player



153
154
155
156
157
158
159
160
161
162
# File 'lib/activefacts/vocabulary/verbaliser.rb', line 153

def add_role_player player, role_ref
  #debug :subscript, "Adding role_ref #{role_ref.object_id} to player #{player.object_id}"
  if jr = role_ref.join_role
    add_join_role(player, jr)
  elsif !player.role_refs.include?(role_ref)
    debug :subscript, "Adding reference to player #{player.object_id} for #{role_ref.role.concept.name} in #{role_ref.role_sequence.describe} with #{role_ref.role_sequence.all_reading.size} readings"
    player.role_refs.push(role_ref)
    @player_by_role_ref[role_ref] = player
  end
end

#add_role_ref(role_ref) ⇒ Object



164
165
166
# File 'lib/activefacts/vocabulary/verbaliser.rb', line 164

def add_role_ref role_ref
  add_role_player(player(role_ref), role_ref)
end

#add_role_refs(role_refs) ⇒ Object

Add RoleRefs to one or more Players, creating players where needed



169
170
171
# File 'lib/activefacts/vocabulary/verbaliser.rb', line 169

def add_role_refs role_refs
  role_refs.each{|rr| add_role_ref(rr) }
end

#alternate_readings(readings) ⇒ Object

All these readings are for the same fact type, and all will be emitted, so the roles cover the same players This is used when verbalising fact types and entity types.



200
201
202
203
204
205
206
# File 'lib/activefacts/vocabulary/verbaliser.rb', line 200

def alternate_readings readings
  readings.map do |reading|
    reading.role_sequence.all_role_ref.sort_by{|rr| rr.role.ordinal}
  end.transpose.each do |role_refs|
    role_refs_have_same_player role_refs
  end
end

#choose_step(next_node) ⇒ Object



496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
# File 'lib/activefacts/vocabulary/verbaliser.rb', line 496

def choose_step(next_node)
  next_steps = @join_steps_by_join_node[next_node]

  # If we don't have a next_node against which we can contract,
  # so just use any join step involving this node, or just any step.
  if next_steps
    if next_step = next_steps.detect { |ns| !ns.is_objectification_step }
      debug :join, "Chose new non-objectification step: #{next_step.describe}"
      return next_step
    end
  end

  if next_step = @join_steps.detect { |ns| !ns.is_objectification_step }
    debug :join, "Chose random non-objectification step: #{next_step.describe}"
    return next_step
  end

  next_step = @join_steps[0]
  if next_step
    debug :join, "Chose new random step from #{join_steps.size}: #{next_step.describe}"
    if next_step.is_objectification_step
      # if this objectification plays any roles (other than its FT roles) in remaining steps, use one of those first:
      fact_type = next_step.fact_type.role.fact_type
      jn = [next_step.input_join_role.join_node, next_step.output_join_role.join_node].detect{|jn| jn.concept == fact_type.entity_type}
      sr = @join_steps_by_join_node[jn].reject{|t| t.fact_type.role and t.fact_type.role.fact_type == fact_type}
      next_step = sr[0] if sr.size > 0 
    end
    return next_step
  end
  raise "Internal error: There are more join steps here, but we failed to choose one"
end

#contractable_step(next_steps, next_node) ⇒ Object

The last reading we emitted ended with the object type name for next_node. Choose a step and a reading that can be contracted against that name



548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
# File 'lib/activefacts/vocabulary/verbaliser.rb', line 548

def contractable_step(next_steps, next_node)
  next_reading = nil
  next_step =
    next_steps.detect do |js|
      next false if js.is_objectification_step
      # If we find a reading here, it can be contracted against the previous one
      next_reading =
        js.fact_type.all_reading_by_ordinal.detect do |reading|
          # This step is contractable iff the FactType has a reading that starts with the role of next_node (no preceding text)
          reading_starts_with_node(reading, next_node)
        end
      next_reading
    end
  debug :join, "#{next_reading ? "'"+next_reading.expand+"'" : "No reading"} contracts against last node '#{next_node.concept.name}'"
  return [next_step, next_reading]
end

#create_subscriptsObject



254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
# File 'lib/activefacts/vocabulary/verbaliser.rb', line 254

def create_subscripts
  # Create subscripts, where necessary
  @players.each { |p| p.subscript = nil } # Wipe subscripts
  @players.
    map{|p| [p, p.concept] }.
    each do |player, concept|
      next if player.subscript  # Done previously
      dups = @players.select{|p| p.concept == concept && p.role_adjuncts == player.role_adjuncts }
      if dups.size == 1
        debug :subscript, "No subscript needed for #{concept.name}"
        next
      end
      debug :subscript, "Applying subscripts to #{dups.size} occurrences of #{concept.name}" do
        dups.each_with_index do |player, index|
          player.subscript = index+1
        end
      end
    end
end

#elided_objectification(next_step, fact_type, last_is_contractable, next_node) ⇒ Object



610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
# File 'lib/activefacts/vocabulary/verbaliser.rb', line 610

def elided_objectification(next_step, fact_type, last_is_contractable, next_node)
  if last_is_contractable
    # Choose a reading that's contractable against the previous step, if possible
    reading = fact_type.all_reading_by_ordinal.
      detect do |reading|
        reading_starts_with_node(reading, next_node)
      end
  end
  last_is_contractable = false unless reading
  reading ||= fact_type.preferred_reading

  # Find which role occurs last in the reading, and which Join Node is attached
  reading.text =~ /\{(\d)\}[^{]*\Z/
  last_role_ref = reading.role_sequence.all_role_ref_in_order[$1.to_i]
  exit_node = @join_nodes.detect{|jn| jn.all_join_role.detect{|jr| jr.role == last_role_ref.role}}
  exit_step = nil

  while other_step =
    @join_steps.
      detect{|js|
        next unless js.is_objectification_step
        next unless js.input_join_role.join_node.concept == fact_type.entity_type || js.output_join_role.join_node.concept == fact_type.entity_type
        exit_step = js if js.output_join_role.join_node == exit_node
        true
      }
    debug :join, "Emitting objectified FT allows deleting #{other_step.describe}"
    step_completed(other_step)
  end
  [ reading, exit_step ? exit_step.input_join_role.join_node : exit_node, exit_step, last_is_contractable]
end

#expand_contracted_text(step, reading, role_refs = []) ⇒ Object



458
459
460
461
# File 'lib/activefacts/vocabulary/verbaliser.rb', line 458

def expand_contracted_text(step, reading, role_refs = [])
  ' that ' +
    expand_reading_text(step, reading.text.sub(/\A\{\d\} /,''), reading.role_sequence, role_refs)
end

#expand_reading(reading, frequency_constraints = [], define_role_names = nil, value_constraints = [], &subscript_block) ⇒ Object

Expand a reading for an entity type or fact type definition. Unlike expansions in constraints, these expansions include frequency constraints, role names and value constraints as passed-in, and also define adjectives by using the hyphenated form (on at least the first occurrence).



277
278
279
280
281
# File 'lib/activefacts/vocabulary/verbaliser.rb', line 277

def expand_reading(reading, frequency_constraints = [], define_role_names = nil, value_constraints = [], &subscript_block)
  reading.expand(frequency_constraints, define_role_names, value_constraints) do |role_ref|
    (!(role_ref.role.role_name and define_role_names) and p = player(role_ref) and p.subscript) ? "(#{p.subscript})" : ""
  end
end

#expand_reading_text(step, text, role_sequence, player_by_role = {}) ⇒ Object

Expand this reading (or partial reading, during contraction)



414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
# File 'lib/activefacts/vocabulary/verbaliser.rb', line 414

def expand_reading_text(step, text, role_sequence, player_by_role = {})
  if !player_by_role.empty? and !player_by_role.is_a?(Hash) || player_by_role.keys.detect{|k| !k.is_a?(ActiveFacts::Metamodel::Role)}
    debugger
    raise "Need to change this call to expand_reading_text to pass a role->join_node hash"
  end
  rrs = role_sequence.all_role_ref_in_order
  debug :subscript, "expanding '#{text}' with #{role_sequence.describe}" do
    text.gsub(/\{(\d)\}/) do
      role_ref = rrs[$1.to_i]
      # REVISIT: We may need to use the step's role_refs to expand the role players here, not the reading's one (extra adjectives?)
      # REVISIT: There's no way to get literals to be emitted here (value join step?)

      player = player_by_role[role_ref.role]

=begin
      rr = role_refs.detect{|rr| rr.role == role_ref.role} || role_ref

      player = @player_by_role_ref[rr] and subscript = player.subscript
      if !subscript and
        pp = @players.select{|p|p.concept == rr.role.concept} and
        pp.detect{|p|p.subscript}
        # raise "Internal error: Subscripted players (of the same concept #{pp[0].concept.name}) when this player isn't subscripted"
      end
=end

      subscripted_player(role_ref, player && player.subscript) +
        objectification_verbalisation(role_ref.role.concept)
    end
  end
end

#identifying_role_names(identifying_role_refs) ⇒ Object

Return an array of the names of these identifying_roles.



174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
# File 'lib/activefacts/vocabulary/verbaliser.rb', line 174

def identifying_role_names identifying_role_refs
  identifying_role_refs.map do |role_ref|
    preferred_role_ref = role_ref.role.fact_type.preferred_reading.role_sequence.all_role_ref.detect{|reading_rr|
      reading_rr.role == role_ref.role
    }

    if (role_ref.role.fact_type.all_role.size == 1)
      role_ref.role.fact_type.default_reading    # Need whole reading for a unary.
    elsif role_name = role_ref.role.role_name and role_name != ''
      role_name
    else
      role_words = []
      role_words << preferred_role_ref.leading_adjective if preferred_role_ref.leading_adjective != ""
      role_words << preferred_role_ref.role.concept.name
      role_words << preferred_role_ref.trailing_adjective if preferred_role_ref.trailing_adjective != ""
      role_name = role_words.compact*"-"
      if p = player(preferred_role_ref) and p.subscript
        role_name += "(#{p.subscript})"
      end
      role_name
    end
  end
end

#join_roles_have_same_player(join_roles) ⇒ Object



208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
# File 'lib/activefacts/vocabulary/verbaliser.rb', line 208

def join_roles_have_same_player join_roles
  return if join_roles.empty?

  # If any of these join_roles are for a known player, use that, else make a new player.
  existing_players = join_roles.map{|jr| @player_by_join_role[jr] }.compact.uniq
  if existing_players.size > 1
    raise "Can't join these roles to more than one existing player: #{existing_players.map{|p|p.concept.name}*', '}!"
  end
  p = existing_players[0] || player(join_roles[0])
  debugger if join_roles.detect{|jr| jr.role.concept != p.concept }
  debug :subscript, "Joining roles to #{p.describe}" do
    join_roles.each do |jr|
      debug :subscript, "#{jr.describe}" do
        add_join_role p, jr
      end
    end
  end
end

#node_contractable_against_reading(next_node, reading) ⇒ Object

The join step we just emitted (using the reading given) is contractable iff the reading has the next_node’s role player as the final text



530
531
532
533
534
535
536
537
538
# File 'lib/activefacts/vocabulary/verbaliser.rb', line 530

def node_contractable_against_reading(next_node, reading)
  reading &&
    # Find whether last role has no following text, and its ordinal
  (reading.text =~ /\{([0-9])\}$/) &&
    # This reading's RoleRef for that role:
  (role_ref = reading.role_sequence.all_role_ref_in_order[$1.to_i]) &&
    # was that RoleRef for the upcoming node?
  role_ref.role.concept == next_node.concept
end

#objectification_verbalisation(concept) ⇒ Object

REVISIT: There might be more than one objectification_verbalisation for a given concept. Need to get the Join Node here and emit an objectification step involving that node.



566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
# File 'lib/activefacts/vocabulary/verbaliser.rb', line 566

def objectification_verbalisation(concept)
  objectified_node = nil
  unless concept.is_a?(Metamodel::EntityType) and
    concept.fact_type and            # Not objectified
    objectification_step = @join_steps.
      detect do |js|
        # The objectifying entity type should always be the input_join_node here, but be safe:
        js.is_objectification_step and
          (objectified_node = js.input_join_role.join_node).concept == concept ||
          (objectified_node = js.output_join_role.join_node).concept == concept
      end
    return ''
  end

  # REVISIT: We need to be working from the role_ref here - pass it in
  # if objectification_step.join_node != role_ref.join_node

  steps = [objectification_step]
  step_completed(objectification_step)
  while other_step =
    @join_steps.
      detect{|js|
        js.is_objectification_step and
          js.input_join_role.join_node.concept == concept || js.output_join_role.join_node.concept == concept
      }
    steps << other_step
    debug :join, "Emitting objectification step allows deleting #{other_step.describe}"
    step_completed(other_step)
  end

  # Find all references to roles in this objectified fact type which are relevant to the join nodes of these steps:
  player_by_role = {}
  steps.each do |join_step|
    join_step.all_join_role.to_a.map do |jr|
      player_by_role[jr.role] = @player_by_join_role[jr]
    end
  end

  # role_refs = steps.map{|step| [step.input_join_role.join_node, step.output_join_role.join_node].map{|jn| jn.all_role_ref.detect{|rr| rr.role.fact_type == concept.fact_type}}}.flatten.compact.uniq

  reading = concept.fact_type.preferred_reading
  " (where #{expand_reading_text(objectification_step, reading.text, reading.role_sequence, player_by_role)})" 
end

#player(ref) ⇒ Object

Find or create a Player to which we can add this role_ref



130
131
132
133
134
135
136
137
138
139
# File 'lib/activefacts/vocabulary/verbaliser.rb', line 130

def player(ref)
  if ref.is_a?(ActiveFacts::Metamodel::JoinRole)
    @player_by_join_role[ref] or
      @players.push(p = Player.new(ref.role.concept)) && p
  else
    @player_by_role_ref[ref] or
      ref.join_role && @player_by_join_role[ref.join_role] or
      @players.push(p = Player.new(ref.role.concept)) && p
  end
end

#prepare_join(join) ⇒ Object

Each join we wish to verbalise must first have had its players prepared. Then, this prepares the join for verbalising:



465
466
467
468
469
470
471
472
473
474
475
476
477
# File 'lib/activefacts/vocabulary/verbaliser.rb', line 465

def prepare_join join
  @join = join
  return unless join

  @join_nodes = join.all_join_node.sort_by{|jn| jn.ordinal}

  @join_steps = @join_nodes.map{|jn| jn.all_join_step }.flatten.uniq
  @join_steps_by_join_node = @join_nodes.
    inject({}) do |h, jn|
      jn.all_join_step.each{|js| (h[jn] ||= []) << js}
      h
    end
end

#prepare_join_players(join) ⇒ Object



329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
# File 'lib/activefacts/vocabulary/verbaliser.rb', line 329

def prepare_join_players join
  debug :subscript, "Indexing roles of fact types in #{join.all_join_step.size} join steps" do
    join_steps = []
    # Register all references to each join node as being for the same player:
    join.all_join_node.sort_by{|jn| jn.ordinal}.each do |join_node|
      debug :subscript, "Adding Roles of #{join_node.describe}" do
        join_roles_have_same_player(join_node.all_join_role.to_a)
        join_steps = join_steps | join_node.all_join_step
      end
    end

=begin
    # For each fact type traversed, register a player for each role *not* linked to this join
    # REVISIT: Using the preferred_reading role_ref is wrong here; the same preferred_reading might occur twice,
    # so the respective concept will need more than one Player and will be subscripted to keep them from being joined.
    # Accordingly, there must be a join step for each such role, and to enforce that, I raise an exception here on duplication.
    # This isn't needed now all JoinNodes have at least one JoinRole

    join_steps.map do |js|
      if js.fact_type.is_a?(ActiveFacts::Metamodel::ImplicitFactType)
        js.fact_type.role.fact_type
      else
        js.fact_type
      end
    end.uniq.each do |fact_type|
    #join_steps.map{|js|js.fact_type}.uniq.each do |fact_type|
      next if fact_type.is_a?(ActiveFacts::Metamodel::ImplicitFactType)

      debug :subscript, "Residual roles in '#{fact_type.default_reading}' are" do
        prrs = fact_type.preferred_reading.role_sequence.all_role_ref
        residual_roles = fact_type.all_role.select{|r| !r.all_role_ref.detect{|rr| rr.join_node && rr.join_node.join == join} }
        residual_roles.each do |r|
          debug :subscript, "Adding residual role for #{r.concept.name} (in #{fact_type.default_reading}) not covered in join"
          preferred_role_ref = prrs.detect{|rr| rr.role == r}
          if p = @player_by_role_ref[preferred_role_ref] and !p.role_refs.include?(preferred_role_ref)
            raise "Adding DUPLICATE residual role for #{r.concept.name} not covered in join"
          end
          role_refs_have_same_player([preferred_role_ref])
        end
      end
    end
=end
  end
end

#prepare_role_sequence(role_sequence, join_over = nil) ⇒ Object



302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
# File 'lib/activefacts/vocabulary/verbaliser.rb', line 302

def prepare_role_sequence role_sequence, join_over = nil
  @role_refs = role_sequence.is_a?(Array) ? role_sequence : role_sequence.all_role_ref.to_a

  if jrr = @role_refs.detect{|rr| rr.join_role && rr.join_role.join_node}
    return prepare_join_players(jrr.join_role.join_node.join)
  end

  # Ensure that all the joined-over role_refs are indexed for subscript generation.
  role_refs_by_fact_type =
    @role_refs.inject({}) { |hash, rr| (hash[rr.role.fact_type] ||= []) << rr; hash }
  role_refs_by_fact_type.each do |fact_type, role_refs|
    role_refs.each { |rr| role_refs_have_same_player([rr]) }

    # Register the role_refs in the preferred reading which refer to roles not covered in the role sequence.
    prrs = fact_type.preferred_reading.role_sequence.all_role_ref
    residual_roles = fact_type.all_role.select{|r| !@role_refs.detect{|rr| rr.role == r} }
    residual_roles.each do |role|
      debug :subscript, "Adding residual role for #{role.concept.name} (in #{fact_type.default_reading}) not covered in role sequence"
      preferred_role_ref = prrs.detect{|rr| rr.role == role}
      if p = @player_by_role_ref[preferred_role_ref] and !p.role_refs.include?(preferred_role_ref)
        raise "Adding DUPLICATE residual role for #{role.concept.name}"
      end
      role_refs_have_same_player([prrs.detect{|rr| rr.role == role}])
    end
  end
end

#reading_starts_with_node(reading, next_node) ⇒ Object



540
541
542
543
544
# File 'lib/activefacts/vocabulary/verbaliser.rb', line 540

def reading_starts_with_node(reading, next_node)
  reading.text =~ /^\{([0-9])\}/ and
    role_ref = reading.role_sequence.all_role_ref.detect{|rr| rr.ordinal == $1.to_i} and
    role_ref.role.concept == next_node.concept
end

#role_refs_are_subtype_joined(roles) ⇒ Object

Where no explicit Join has been created, a join is still sometimes present (e.g. in a constraint from NORMA) REVISIT: This probably doesn’t produce the required result. Need to fix the NORMA importer to create the join.



285
286
287
288
289
# File 'lib/activefacts/vocabulary/verbaliser.rb', line 285

def role_refs_are_subtype_joined roles
  role_refs = roles.is_a?(Array) ? roles : roles.all_role_ref.to_a
  role_refs_by_concept = role_refs.inject({}) { |h, r| (h[r.role.concept] ||= []) << r; h }
  role_refs_by_concept.values.each { |rrs|  role_refs_have_same_player(rrs) }
end

#role_refs_have_same_player(role_refs) ⇒ Object

These RoleRefs are all for the same player. Find whether any of them has a player already



228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
# File 'lib/activefacts/vocabulary/verbaliser.rb', line 228

def role_refs_have_same_player role_refs
  role_refs = role_refs.is_a?(Array) ? role_refs : role_refs.all_role_ref.to_a
  return if role_refs.empty?

  # If any of these role_refs are for a known player, use that, else make a new player.
  existing_players =
    role_refs.map{|rr| @player_by_role_ref[rr] || @player_by_join_role[rr.join_role] }.compact.uniq
  if existing_players.size > 1
    raise "Can't join these role_refs to more than one existing player: #{existing_players.map{|p|p.concept.name}*', '}!"
  end
  p = existing_players[0] || player(role_refs[0])

  debug :subscript, "#{existing_players[0] ? 'Adding to existing' : 'Creating new'} player for #{role_refs.map{|rr| rr.role.concept.name}.uniq*', '}" do
    role_refs.each do |rr|
      unless p.concept == rr.role.concept
        # This happens in SubtypePI because uniqueness constraint is built without its implicit subtyping join.
        # For now, explode only if there's no common supertype:
        if 0 == (p.concept.supertypes_transitive & rr.role.concept.supertypes_transitive).size
          raise "REVISIT: Internal error, trying to add role of #{rr.role.concept.name} to player #{p.concept.name}"
        end
      end
      add_role_player(p, rr)
    end
  end
end

#roles_have_same_player(roles) ⇒ Object

These roles are the players in an implicit counterpart join in a Presence Constraint. REVISIT: It’s not clear that we can safely use the preferred_reading’s RoleRefs here. Fix the CQL compiler to create proper joins for these presence constraints instead.



294
295
296
297
298
299
300
# File 'lib/activefacts/vocabulary/verbaliser.rb', line 294

def roles_have_same_player roles
  role_refs = roles.map do |role|
    pr = role.fact_type.preferred_reading
    pr.role_sequence.all_role_ref.detect{|rr| rr.role == role}
  end
  role_refs_have_same_player(role_refs)
end

#step_completed(step) ⇒ Object

Remove this step now that we’ve processed it:



480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
# File 'lib/activefacts/vocabulary/verbaliser.rb', line 480

def step_completed(step)
  @join_steps.delete(step)

  input_node = step.input_join_role.join_node
  steps = @join_steps_by_join_node[input_node]
  steps.delete(step)
  @join_steps_by_join_node.delete(input_node) if steps.empty?

  output_node = step.output_join_role.join_node
  if (input_node != output_node)
    steps = @join_steps_by_join_node[output_node]
    steps.delete(step)
    @join_steps_by_join_node.delete(output_node) if steps.empty?
  end
end

#subscripted_player(role_ref, subscript = nil) ⇒ Object



445
446
447
448
449
450
451
452
453
454
455
456
# File 'lib/activefacts/vocabulary/verbaliser.rb', line 445

def subscripted_player role_ref, subscript = nil
  if subscript
    debug :subscript, "Need to apply subscript #{subscript} to #{role_ref.role.concept.name}"
  end
  concept = role_ref.role.concept
  [
    role_ref.leading_adjective,
    concept.name,
    role_ref.trailing_adjective
  ].compact*' ' +
    (subscript ? "(#{subscript})" : '')
end

#verbalise_join(join) ⇒ Object



641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
# File 'lib/activefacts/vocabulary/verbaliser.rb', line 641

def verbalise_join join
  prepare_join join
  readings = ''
  next_node = @role_refs[0].join_role.join_node   # Choose a place to start
  last_is_contractable = false
  debug :join, "Join Nodes are #{@join_nodes.map{|jn| jn.describe }.inspect}, Join Steps are #{@join_steps.map{|js| js.describe }.inspect}" do
    until @join_steps.empty?
      next_reading = nil
      # Choose amonst all remaining steps we can take from the next node, if any
      next_steps = @join_steps_by_join_node[next_node]
      debug :join, "Next Steps from #{next_node.describe} are #{(next_steps||[]).map{|js| js.describe }.inspect}"

      # See if we can find a next step that contracts against the last (if any):
      next_step = nil
      if last_is_contractable && next_steps
        next_step, next_reading = *contractable_step(next_steps, next_node)
          end

      if next_step
        debug :join, "Chose #{next_step.describe} because it's contractable against last node #{next_node.concept.name} using #{next_reading.expand}"

        player_by_role =
          next_step.all_join_role.inject({}) {|h, jr| h[jr.role] = @player_by_join_role[jr]; h }
        readings += expand_contracted_text(next_step, next_reading, player_by_role)
        step_completed(next_step)
      else
        next_step = choose_step(next_node) if !next_step

        player_by_role =
          next_step.all_join_role.inject({}) {|h, jr| h[jr.role] = @player_by_join_role[jr]; h }

        if next_step.is_unary_step
          # Objectified unaries get emitted as unaries, not as objectifications:
          # REVISIT: There must be a simpler way of finding the preferred reading here:
          rr = next_step.input_join_node.all_role_ref.detect{|rr| rr.role.fact_type.is_a?(ImplicitFactType) }
          next_reading = rr.role.fact_type.role.fact_type.preferred_reading
          readings += " and " unless readings.empty?
          readings += expand_reading_text(next_step, next_reading.text, next_reading.role_sequence, player_by_role)
          step_completed(next_step)
        elsif next_step.is_objectification_step
          fact_type = next_step.fact_type.role.fact_type

          # This objectification step is over an implicit fact type, so player_by_role won't have all the players
          # Add the players of other roles associated with steps from this objectified player.
          objectified_node = next_step.input_join_role.join_node
          raise "Assumption violated that the objectification is the input join role" unless objectified_node.concept.fact_type
          objectified_node.all_join_step.map do |other_step|
            (other_step.all_incidental_join_role.to_a + [other_step.output_join_role]).map do |jr|
              player_by_role[jr.role] = @player_by_join_role[jr]
            end
          end

          if last_is_contractable and next_node.concept.is_a?(EntityType) and next_node.concept.fact_type == fact_type
            # The last reading we emitted ended with the name of the objectification of this fact type, so we can contract the objectification
            # REVISIT: Do we need to use player_by_role here (if this objectification is traversed twice and so is subscripted)
            readings += objectification_verbalisation(fact_type.entity_type)
          else
            # This objectified fact type does not need to be made explicit.
            next_reading, next_node, next_step, last_is_contractable =
              *elided_objectification(next_step, fact_type, last_is_contractable, next_node)
            if last_is_contractable
              readings += expand_contracted_text(next_step, next_reading, player_by_role)
            else
              readings += " and " unless readings.empty?
              readings += expand_reading_text(next_step, next_reading.text, next_reading.role_sequence, player_by_role)
            end
            # No need to continue if we just deleted the last step
            break if @join_steps.empty?

          end
        else
          fact_type = next_step.fact_type
          # Prefer a reading that starts with the player of next_node
          next_reading = fact_type.all_reading_by_ordinal.
            detect do |reading|
              reading_starts_with_node(reading, next_node)
            end || fact_type.preferred_reading
          # REVISIT: If this join step and reading has role references with adjectives, we need to expand using those
          readings += " and " unless readings.empty?
          readings += expand_reading_text(next_step, next_reading.text, next_reading.role_sequence, player_by_role)
          step_completed(next_step)
        end
      end

      # Continue from this step with the node having the most steps remaining
      input_steps = @join_steps_by_join_node[input_node = next_step.input_join_role.join_node] || []
      output_steps = @join_steps_by_join_node[output_node = next_step.output_join_role.join_node] || []
      next_node = input_steps.size > output_steps.size ? input_node : output_node
      # Prepare for possible contraction following:
      last_is_contractable = next_reading && node_contractable_against_reading(next_node, next_reading)

    end
  end
  readings
end

#verbalise_over_role_sequence(role_sequence, joiner = ' and ', role_proximity = :both) ⇒ Object



374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
# File 'lib/activefacts/vocabulary/verbaliser.rb', line 374

def verbalise_over_role_sequence role_sequence, joiner = ' and ', role_proximity = :both
  @role_refs = role_sequence.is_a?(Array) ? role_sequence : role_sequence.all_role_ref.to_a

  if jrr = role_refs.detect{|rr| rr.join_role}
    return verbalise_join(jrr.join_role.join_node.join)
  end

  # First, figure out whether there's a join:
  join_over, joined_roles = *Metamodel.join_roles_over(role_sequence.all_role_ref.map{|rr|rr.role}, role_proximity)

  role_by_fact_type = {}
  fact_types = @role_refs.map{|rr| ft = rr.role.fact_type; role_by_fact_type[ft] ||= rr.role; ft}.uniq
  readings = fact_types.map do |fact_type|
    name_substitutions = []
    # Choose a reading that start with the (first) role which caused us to emit this fact type:
    reading = fact_type.reading_preferably_starting_with_role(role_by_fact_type[fact_type])
    if join_over and      # Find a reading preferably starting with the joined_over role:
      joined_role = fact_type.all_role.select{|r| join_over.subtypes_transitive.include?(r.concept)}[0]
      reading = fact_type.reading_preferably_starting_with_role joined_role

      # Use the name of the joined_over object, not the role player, in case of a subtype join:
      rrrs = reading.role_sequence.all_role_ref_in_order
      role_index = (0..rrrs.size).detect{|i| rrrs[i].role == joined_role }
      name_substitutions[role_index] = [nil, join_over.name]
    end
    reading.role_sequence.all_role_ref.each do |rr|
      next unless player = @player_by_role_ref[rr]
      next unless subscript = player.subscript
      debug :subscript, "Need to apply subscript #{subscript} to #{rr.role.concept.name}"
    end
    player_by_role = {}
    @player_by_role_ref.keys.each{|rr| player_by_role[rr.role] = @player_by_role_ref[rr] if rr.role.fact_type == fact_type }
    #role_refs = @player_by_role_ref.keys.select{|rr| rr.role.fact_type == fact_type}
    expand_reading_text(nil, reading.text, reading.role_sequence, player_by_role)
    #reading.expand(name_substitutions)
  end
  joiner ? readings*joiner : readings
end