Class: Combination

Inherits:
TaxonName
  • Object
show all
Includes:
Shared::Citable
Defined in:
app/models/combination.rb

Overview

A Combination has no name, it exists to group related Protonyms into an epithet.

A nomenclator name, composed of existing Protonyms. Each record reflects the subsequent use of two or more protonyms.

Only the first use of a combination is stored here, subsequence uses of this combination are referenced in citations.

They are applicable to genus group names and finer epithets.

All elements of the combination must be defined, nothing is assumed based on the relationhip to the parent.

c = Combination.new
c.genus = a_protonym_genus  
c.species = a_protonym_species
c.save # => true
c.genus_taxon_name_relationship  # => A instance of TaxonNameRelationship::Combination::Genus

# or

c = Combination.new(genus: genus_protonym, species: species_protonym)

Getters and setters for each of the APPLICABLE_RANKS are available:

genus subgenus section subsection series subseries species subspecies variety subvariety form subform
genus_id subgenus_id section_id subsection_id series_id subseries_id species_id subspecies_id variety_id subvariety_id form_id subform_id

You can things like (notice mix/match of _id or not):

c = Combination.new(genus_id: @genus_protonym.id, subspecies: @some_species_group)
c.species_id = Protonym.find(some_species_id).id

or

c.species = Protonym.find(some_species_id)

Constant Summary

APPLICABLE_RANKS =

The ranks that can be used to build combinations.

%w{genus subgenus section subsection series subseries species subspecies variety subvariety form subform}

Constants inherited from TaxonName

TaxonName::ALTERNATE_VALUES_FOR, TaxonName::EXCEPTED_FORM_TAXON_NAME_CLASSIFICATIONS, TaxonName::NOT_LATIN, TaxonName::NO_CACHED_MESSAGE, TaxonName::SPECIES_EPITHET_RANKS

Constants included from SoftValidation

SoftValidation::ANCESTORS_WITH_SOFT_VALIDATIONS

Instance Attribute Summary (collapse)

Attributes inherited from TaxonName

#also_create_otu, #cached, #cached_author_year, #cached_classified_as, #cached_higher_classification, #cached_html, #cached_misspelling, #cached_original_combination, #cached_primary_homonym, #cached_primary_homonym_alternative_spelling, #cached_secondary_homonym, #cached_secondary_homonym_alternative_spelling, #feminine_name, #masculine_name, #name, #neuter_name, #no_cached, #project_id, #rank_class, #type, #verbatim_author, #verbatim_name, #year_of_publication

Instance Method Summary (collapse)

Methods inherited from TaxonName

#all_taxon_name_relationships, #ancestor_at_rank, #ancestor_protonyms, #ancestors_through_parents, #author_string, #cached_html_name_and_author_year, #cached_name_and_author_year, #check_for_children, #check_new_parent_class, #check_new_rank_class, #classification_invalid_or_unavailable?, #classification_valid?, #combination_list_all, #combination_list_self, #combined_statuses, #create_new_combination_if_absent, #descendant_protonyms, #first_possible_valid_taxon_name, #first_possible_valid_taxon_name_relationship, #form_name_elements, #fossil?, #full_name_array, #gbif_status_array, #gender_class, #gender_instance, #gender_name, #genus_name_elements, #get_cached_classified_as, #get_cached_misspelling, #get_full_name, #get_genus_species, #get_original_combination, #hybrid?, #icn_author_and_year, #iczn_author_and_year, #is_combination?, #is_protonym?, #is_valid?, #list_of_invalid_taxon_names, #name_in_gender, #name_is_missapplied?, #name_with_misspelling, #next_sibling, #nomenclature_date, #parent_is_set?, #part_of_speech_class, #part_of_speech_instance, #part_of_speech_name, #previous_sibling, #rank, #rank_string, #related_taxon_names, #relationship_invalid?, #safe_self_and_ancestors, #section_name_elements, #series_name_elements, #set_cached_author_year, #set_cached_classified_as, #set_cached_names, #set_cached_names_for_dependants_and_self, #set_cached_original_combination, #set_type_if_empty, sort_by_rank, #species_group_name_elements, #species_name_elements, #statuses_from_classifications, #statuses_from_relationships, #subform_name_elements, #subgenus_name_elements, #subsection_name_elements, #subseries_name_elements, #subspecies_name_elements, #subvariety_name_elements, #superspecies_name_elements, #sv_cached_names, #sv_fix_cached_names, #sv_fix_missing_author, #sv_fix_missing_year, #sv_fix_parent_is_valid_name, #sv_homotypic_synonyms, #sv_hybrid_name_relationships, #sv_missing_classifications, #sv_missing_fields, #sv_missing_relationships, #sv_not_synonym_of_self, #sv_parent_is_valid_name, #sv_parent_priority, #sv_potential_homonyms, #sv_primary_types, #sv_single_sub_taxon, #sv_species_gender_agreement, #sv_two_unresolved_alternative_synonyms, #sv_type_placement, #sv_validate_coordinated_names, #sv_validate_name, #sv_validate_parent_rank, #synonyms, #taxon_name_classifications_for_statuses, #unavailable_or_invalid?, #update_cached_original_combinations, #validate_one_root_per_project, #validate_parent_is_set, #validate_parent_rank_is_higher, #validate_source_type, #variety_name_elements, with_taxon_name_relationship, #year_integer

Methods included from SoftValidation

#clear_soft_validations, #fix_soft_validations, #soft_fixed?, #soft_valid?, #soft_validate, #soft_validated?, #soft_validations

Methods included from Housekeeping

#has_polymorphic_relationship?

Instance Attribute Details

- (String) combination_verbatim_name

Use with caution, and sparingly! If the combination of values from Protonyms can not reflect the formulation of the combination as provided by the original author that string can be provided here. The verbatim value is not further parsed. It is only provided to clarify what the combination looked like when first published. The following recommendations are made:

1) The provided string should visually reflect as close as possible what was seen in the publication itself, including 
capitalization, accented characters etc. 
2) The full epithet (combination) should be provided, not just the differing component part (see 3 below).
3) Misspellings can be more acurately reflected by creating new Protonyms.

Example uses:

1) Jones 1915 publishes Aus aus. Smith 1920 uses, literally "Aus (Bus) Janes 1915". 
   It is clear "Janes" is "Jones", therefor "Aus (Bus) Janes 1915" is provided as combination_verbatim_name.
2) Smith 1800 publishes Aus Jonesi (i.e. Aus jonesi). The combination_combination_verbatim name is used to
   provide the fact that Jonesi was capitalized.  
3) "Aus brocen" is used for "Aus broken".  If the curators decide not to create a new protonym, perhaps because
   they feel "brocen" was a printing press error that left off the straight bit of the "k" then they should minimally
   include "Aus brocen" in this field, rather than just "brocen". An alternative is to create a new Protonym "brocen".

Returns:

  • (String)


51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
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
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
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
328
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
# File 'app/models/combination.rb', line 51

class Combination < TaxonName

  include Shared::Citable

  # The ranks that can be used to build combinations.
  APPLICABLE_RANKS = %w{genus subgenus section subsection series subseries species subspecies variety subvariety form subform}

  before_validation :set_parent

  has_many :combination_relationships, -> {
    joins(:taxon_name_relationships)
    where("taxon_name_relationships.type LIKE 'TaxonNameRelationship::Combination::%'")
  }, class_name: 'TaxonNameRelationship', foreign_key: :object_taxon_name_id

  has_many :combination_relationships_as_subject, -> {
    joins(:taxon_name_relationships)
    where("taxon_name_relationships.type LIKE 'TaxonNameRelationship::Combination::%'")
  }, class_name: 'TaxonNameRelationship', foreign_key: :subject_taxon_name_id

  has_many :combination_taxon_names, through: :combination_relationships, source: :subject_taxon_name

  TaxonNameRelationship.descendants.each do |d|
    if d.respond_to?(:assignment_method)
      if d.name.to_s =~ /TaxonNameRelationship::SourceClassifiedAs/
        relationship = "#{d.assignment_method}_relationship".to_sym
        has_one relationship, class_name: d.name.to_s, foreign_key: :subject_taxon_name_id
        has_one d.assignment_method.to_sym, through: relationship, source: :object_taxon_name
      end

      if d.name.to_s =~ /TaxonNameRelationship::Combination/ # |SourceClassifiedAs
        relationships = "#{d.assignment_method}_relationships".to_sym
        has_many relationships, -> {
          where("taxon_name_relationships.type LIKE '#{d.name.to_s}%'")
        }, class_name: 'TaxonNameRelationship', foreign_key: :subject_taxon_name_id
        has_many d.assignment_method.to_s.pluralize.to_sym, through: relationships, source: :object_taxon_name
      end
    end

    if d.respond_to?(:inverse_assignment_method)
      if d.name.to_s =~ /TaxonNameRelationship::SourceClassifiedAs/
        relationships = "#{d.inverse_assignment_method}_relationships".to_sym
        has_many relationships, -> {
          where("taxon_name_relationships.type LIKE '#{d.name.to_s}%'")
        }, class_name: 'TaxonNameRelationship', foreign_key: :object_taxon_name_id
        has_many d.inverse_assignment_method.to_s.pluralize.to_sym, through: relationships, source: :subject_taxon_name
      end

      if d.name.to_s =~ /TaxonNameRelationship::Combination/ # |SourceClassifiedAs
        relationship = "#{d.inverse_assignment_method}_relationship".to_sym
        has_one relationship, class_name: d.name.to_s, foreign_key: :object_taxon_name_id
        has_one d.inverse_assignment_method.to_sym, through: relationship, source: :subject_taxon_name
      end
    end
  end


  APPLICABLE_RANKS.each do |rank|
    has_one "#{rank}_taxon_name_relationship".to_sym, -> {
      joins(:combination_relationships)
      where(taxon_name_relationships: {type: "TaxonNameRelationship::Combination::#{rank.capitalize}"}) },
    class_name: 'TaxonNameRelationship', foreign_key: :object_taxon_name_id

    has_one rank.to_sym, -> {
      joins(:combination_relationships)
      where(taxon_name_relationships: {type: "TaxonNameRelationship::Combination::#{rank.capitalize}"})
    }, through: "#{rank}_taxon_name_relationship".to_sym, source: :subject_taxon_name

    accepts_nested_attributes_for rank.to_sym
  
    attr_accessor "#{rank}_id".to_sym
    method = "#{rank}_id"

    define_method(method) { 
      if self.send(rank)
        self.send(rank).id
      else
        nil
      end 
    }

    define_method("#{method}=") {|value|
      if !value.blank?
        if n = Protonym.find(value)
          self.send("#{rank}=", n) 
        end
      end
    } 
  end

  scope :with_cached_html, -> (html) { where(cached_html: html) }
  scope :with_protonym_at_rank, -> (rank, protonym) { includes(:combination_relationships).where('taxon_name_relationships.type = ? and taxon_name_relationships.subject_taxon_name_id = ?', rank, protonym).references(:combination_relationships)}

  validate :at_least_two_protonyms_are_included,
    :parent_is_properly_set

  soft_validate(:sv_combination_duplicates, set: :combination_duplicates)
  soft_validate(:sv_year_of_publication_matches_source, set: :dates)
  soft_validate(:sv_year_of_publication_not_older_than_protonyms, set: :dates)
  soft_validate(:sv_source_not_older_than_protonyms, set: :dates)

  # @return [Array of TaxonName]
  #   pre-ordered by rank 
  def protonyms 
    if self.new_record?
      protonyms_by_association
    else
      self.combination_taxon_names.sort{|a,b| RANKS.index(a.rank_string) <=> RANKS.index(b.rank_string)}  # .ordered_by_rank
    end
  end

  # Overrides {TaxonName#full_name_hash}  
  # @return [Hash]
  def full_name_hash
    gender = nil
    data   = {}
    protonyms_by_rank.each do |rank, name|
      gender = name.gender_name if rank == 'genus'
      method = "#{rank.gsub(/\s/, '_')}_name_elements"
      data[rank] = send(method, name, gender) if self.respond_to?(method)
    end
    data
  end

  # @return [Array of TaxonNames, nil]
  #   the component names for this combination prior to it being saved (used to return values prior to save)
  def protonyms_by_rank
    result = {}
    APPLICABLE_RANKS.each do |rank|
      if protonym = self.send(rank)
        result[rank] = protonym
      end
    end
    result
  end

  # @return [Array of Integer]
  #   the collective years the protonyms were (nomenclaturaly) published on (ordered from genus to below)
  def publication_years 
    description_years = protonyms.collect{|a| a.nomenclature_date ? a.nomenclature_date.year : nil}.compact
  end

  # @return [Integer, nil]
  #   the earliest year (nomenclature) that a component Protonym was published on 
  def earliest_protonym_year
    publication_years.sort.first
  end

  # return [Array of TaxonNameRelationship]
  #   classes that are applicable to this name, as deterimned by Rank
  def combination_class_relationships(rank_string)
    relations = []
    TaxonNameRelationship::Combination.descendants.each do |r|
      relations.push(r) if r.valid_object_ranks.include?(rank_string)
    end
    relations
  end

  def combination_relationships_and_stubs(rank_string)
    display_order = [
        :combination_genus, :combination_subgenus, :combination_species, :combination_subspecies, :combination_variety, :combination_form
    ]

    defined_relations = self.combination_relationships.all
    created_already = defined_relations.collect{|a| a.class}
    new_relations = []

    combination_class_relationships(rank_string).each do |r|
      new_relations.push( r.new(object_taxon_name: self) ) if !created_already.include?(r)
    end

    (new_relations + defined_relations).sort{|a,b|
      display_order.index(a.class.inverse_assignment_method) <=> display_order.index(b.class.inverse_assignment_method)
    }
  end

  def set_cached_valid_taxon_name_id
    begin
      TaxonName.transaction do
        self.update_column(:cached_valid_taxon_name_id, self.get_valid_taxon_name.id)
      end
    rescue
    end
  end

  def get_valid_taxon_name
    c = self.protonyms_by_rank
    return self if c.blank?
    c[c.keys.last].valid_taxon_name
  end

  def get_author_and_year
    ay = iczn_author_and_year
    ay.blank? ? nil : ay
  end

  def get_full_name_html
    eo = '<i>'
    ec = '</i>'
    return "#{eo}#{verbatim_name}#{ec}".gsub(' f. ', ec + ' f. ' + eo).gsub(' var. ', ec + ' var. ' + eo) if !self.verbatim_name.nil?
    d = full_name_hash

    elements = []

    elements.push("#{eo}#{d['genus'][1]}#{ec}") if d['genus']
    elements.push ['(', %w{subgenus section subsection series subseries}.collect { |r| d[r] ? [d[r][0], "#{eo}#{d[r][1]}#{ec}"] : nil }, ')']
    elements.push ['(', eo, d['superspecies'][1], ec, ')'] if d['superspecies']

    %w{species subspecies variety subvariety form subform}.each do |r|
      elements.push(d[r][0], "#{eo}#{d[r][1]}#{ec}") if d[r]
    end

    html = elements.flatten.compact.join(' ').gsub(/\(\s*\)/, '').gsub(/\(\s/, '(').gsub(/\s\)/, ')').squish.gsub(' [sic]', ec + ' [sic]' + eo).gsub(ec + ' ' + eo, ' ').gsub(eo + ec, '').gsub(eo + ' ', ' ' + eo)
    html
  end


  protected

  # @return [Array of TaxonNames, nil]
  #   return the component names for this combination prior to it being saved
  def protonyms_by_association
    APPLICABLE_RANKS.collect{|r| self.send(r)}.compact
  end

  # TODO: this is a TaxonName level validation, it doesn't belong here 
  def sv_year_of_publication_matches_source
    source_year = self.source.nomenclature_year if self.source
    if self.year_of_publication && source_year
      soft_validations.add(:year_of_publication, 'the asserted published date is not the same as provided by the source') if source_year != self.year_of_publication
    end
  end

  def sv_source_not_older_than_protonyms
    source_year = self.source.nomenclature_year if self.source
    target_year = earliest_protonym_year
    if source_year && target_year
      soft_validations.add(:base, 'the published date for the source is older than a name in the combination') if source_year < target_year
    end
  end

  def sv_year_of_publication_not_older_than_protonyms
    combination_year = self.year_of_publication
    target_year = earliest_protonym_year
    if combination_year && target_year
      soft_validations.add(:year_of_publication, 'the asserted published date is older than a name in the combination') if combination_year < target_year
    end
  end

  def sv_combination_duplicates
    duplicate = Combination.not_self(self).with_cached_html(self.cached_html)
#    duplicate = Combination.not_self(self).with_parent_id(self.parent_id).with_cached_html(self.cached_html)
    soft_validations.add(:base, 'Combination is a duplicate') unless duplicate.empty?
  end

  def set_parent
    names = self.protonyms 
    if names.count > 0
      self.parent = names.first.parent if names.first.parent
    end
  end

  def set_cached
    write_attribute(:cached, get_full_name) unless self.no_cached
  end

  def set_cached_html
    write_attribute(:cached_html, get_full_name_html) unless self.no_cached
  end

  # validations

  # The parent of a Combination is the parent of the highest ranked protonym in that combination 
  def parent_is_properly_set
    check = protonyms.first
    if self.parent && check && check.parent
      errors.add(:base, 'Parent is not highest ranked member') if  self.parent != check.parent  
    end
  end

  def at_least_two_protonyms_are_included
    c = protonyms.count

    if c == 0
      errors.add(:base, 'Combination includes no taxa, it is not valid')
    else
      rank = protonyms.last.rank_string

      if rank =~/Species/
        errors.add(:base, 'Combination includes only one taxon, it is not valid') if c < 2
      elsif rank =~/Genus/
        errors.add(:base, 'Combination includes more than two taxa, it is not valid') if c > 2
      else
        errors.add(:base, 'Combination includes more than one taxon, it is not valid') if c > 1
      end
    end
  end

  
  def validate_rank_class_class
    errors.add(:rank_class, 'Combination should not have rank') if !!self.rank_class
  end

end

- (Integer) parent_id

the parent is the parent of the highest ranked component protonym, it is automatically set i.e. it should never be assigned directly

Returns:

  • (Integer)


51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
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
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
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
328
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
# File 'app/models/combination.rb', line 51

class Combination < TaxonName

  include Shared::Citable

  # The ranks that can be used to build combinations.
  APPLICABLE_RANKS = %w{genus subgenus section subsection series subseries species subspecies variety subvariety form subform}

  before_validation :set_parent

  has_many :combination_relationships, -> {
    joins(:taxon_name_relationships)
    where("taxon_name_relationships.type LIKE 'TaxonNameRelationship::Combination::%'")
  }, class_name: 'TaxonNameRelationship', foreign_key: :object_taxon_name_id

  has_many :combination_relationships_as_subject, -> {
    joins(:taxon_name_relationships)
    where("taxon_name_relationships.type LIKE 'TaxonNameRelationship::Combination::%'")
  }, class_name: 'TaxonNameRelationship', foreign_key: :subject_taxon_name_id

  has_many :combination_taxon_names, through: :combination_relationships, source: :subject_taxon_name

  TaxonNameRelationship.descendants.each do |d|
    if d.respond_to?(:assignment_method)
      if d.name.to_s =~ /TaxonNameRelationship::SourceClassifiedAs/
        relationship = "#{d.assignment_method}_relationship".to_sym
        has_one relationship, class_name: d.name.to_s, foreign_key: :subject_taxon_name_id
        has_one d.assignment_method.to_sym, through: relationship, source: :object_taxon_name
      end

      if d.name.to_s =~ /TaxonNameRelationship::Combination/ # |SourceClassifiedAs
        relationships = "#{d.assignment_method}_relationships".to_sym
        has_many relationships, -> {
          where("taxon_name_relationships.type LIKE '#{d.name.to_s}%'")
        }, class_name: 'TaxonNameRelationship', foreign_key: :subject_taxon_name_id
        has_many d.assignment_method.to_s.pluralize.to_sym, through: relationships, source: :object_taxon_name
      end
    end

    if d.respond_to?(:inverse_assignment_method)
      if d.name.to_s =~ /TaxonNameRelationship::SourceClassifiedAs/
        relationships = "#{d.inverse_assignment_method}_relationships".to_sym
        has_many relationships, -> {
          where("taxon_name_relationships.type LIKE '#{d.name.to_s}%'")
        }, class_name: 'TaxonNameRelationship', foreign_key: :object_taxon_name_id
        has_many d.inverse_assignment_method.to_s.pluralize.to_sym, through: relationships, source: :subject_taxon_name
      end

      if d.name.to_s =~ /TaxonNameRelationship::Combination/ # |SourceClassifiedAs
        relationship = "#{d.inverse_assignment_method}_relationship".to_sym
        has_one relationship, class_name: d.name.to_s, foreign_key: :object_taxon_name_id
        has_one d.inverse_assignment_method.to_sym, through: relationship, source: :subject_taxon_name
      end
    end
  end


  APPLICABLE_RANKS.each do |rank|
    has_one "#{rank}_taxon_name_relationship".to_sym, -> {
      joins(:combination_relationships)
      where(taxon_name_relationships: {type: "TaxonNameRelationship::Combination::#{rank.capitalize}"}) },
    class_name: 'TaxonNameRelationship', foreign_key: :object_taxon_name_id

    has_one rank.to_sym, -> {
      joins(:combination_relationships)
      where(taxon_name_relationships: {type: "TaxonNameRelationship::Combination::#{rank.capitalize}"})
    }, through: "#{rank}_taxon_name_relationship".to_sym, source: :subject_taxon_name

    accepts_nested_attributes_for rank.to_sym
  
    attr_accessor "#{rank}_id".to_sym
    method = "#{rank}_id"

    define_method(method) { 
      if self.send(rank)
        self.send(rank).id
      else
        nil
      end 
    }

    define_method("#{method}=") {|value|
      if !value.blank?
        if n = Protonym.find(value)
          self.send("#{rank}=", n) 
        end
      end
    } 
  end

  scope :with_cached_html, -> (html) { where(cached_html: html) }
  scope :with_protonym_at_rank, -> (rank, protonym) { includes(:combination_relationships).where('taxon_name_relationships.type = ? and taxon_name_relationships.subject_taxon_name_id = ?', rank, protonym).references(:combination_relationships)}

  validate :at_least_two_protonyms_are_included,
    :parent_is_properly_set

  soft_validate(:sv_combination_duplicates, set: :combination_duplicates)
  soft_validate(:sv_year_of_publication_matches_source, set: :dates)
  soft_validate(:sv_year_of_publication_not_older_than_protonyms, set: :dates)
  soft_validate(:sv_source_not_older_than_protonyms, set: :dates)

  # @return [Array of TaxonName]
  #   pre-ordered by rank 
  def protonyms 
    if self.new_record?
      protonyms_by_association
    else
      self.combination_taxon_names.sort{|a,b| RANKS.index(a.rank_string) <=> RANKS.index(b.rank_string)}  # .ordered_by_rank
    end
  end

  # Overrides {TaxonName#full_name_hash}  
  # @return [Hash]
  def full_name_hash
    gender = nil
    data   = {}
    protonyms_by_rank.each do |rank, name|
      gender = name.gender_name if rank == 'genus'
      method = "#{rank.gsub(/\s/, '_')}_name_elements"
      data[rank] = send(method, name, gender) if self.respond_to?(method)
    end
    data
  end

  # @return [Array of TaxonNames, nil]
  #   the component names for this combination prior to it being saved (used to return values prior to save)
  def protonyms_by_rank
    result = {}
    APPLICABLE_RANKS.each do |rank|
      if protonym = self.send(rank)
        result[rank] = protonym
      end
    end
    result
  end

  # @return [Array of Integer]
  #   the collective years the protonyms were (nomenclaturaly) published on (ordered from genus to below)
  def publication_years 
    description_years = protonyms.collect{|a| a.nomenclature_date ? a.nomenclature_date.year : nil}.compact
  end

  # @return [Integer, nil]
  #   the earliest year (nomenclature) that a component Protonym was published on 
  def earliest_protonym_year
    publication_years.sort.first
  end

  # return [Array of TaxonNameRelationship]
  #   classes that are applicable to this name, as deterimned by Rank
  def combination_class_relationships(rank_string)
    relations = []
    TaxonNameRelationship::Combination.descendants.each do |r|
      relations.push(r) if r.valid_object_ranks.include?(rank_string)
    end
    relations
  end

  def combination_relationships_and_stubs(rank_string)
    display_order = [
        :combination_genus, :combination_subgenus, :combination_species, :combination_subspecies, :combination_variety, :combination_form
    ]

    defined_relations = self.combination_relationships.all
    created_already = defined_relations.collect{|a| a.class}
    new_relations = []

    combination_class_relationships(rank_string).each do |r|
      new_relations.push( r.new(object_taxon_name: self) ) if !created_already.include?(r)
    end

    (new_relations + defined_relations).sort{|a,b|
      display_order.index(a.class.inverse_assignment_method) <=> display_order.index(b.class.inverse_assignment_method)
    }
  end

  def set_cached_valid_taxon_name_id
    begin
      TaxonName.transaction do
        self.update_column(:cached_valid_taxon_name_id, self.get_valid_taxon_name.id)
      end
    rescue
    end
  end

  def get_valid_taxon_name
    c = self.protonyms_by_rank
    return self if c.blank?
    c[c.keys.last].valid_taxon_name
  end

  def get_author_and_year
    ay = iczn_author_and_year
    ay.blank? ? nil : ay
  end

  def get_full_name_html
    eo = '<i>'
    ec = '</i>'
    return "#{eo}#{verbatim_name}#{ec}".gsub(' f. ', ec + ' f. ' + eo).gsub(' var. ', ec + ' var. ' + eo) if !self.verbatim_name.nil?
    d = full_name_hash

    elements = []

    elements.push("#{eo}#{d['genus'][1]}#{ec}") if d['genus']
    elements.push ['(', %w{subgenus section subsection series subseries}.collect { |r| d[r] ? [d[r][0], "#{eo}#{d[r][1]}#{ec}"] : nil }, ')']
    elements.push ['(', eo, d['superspecies'][1], ec, ')'] if d['superspecies']

    %w{species subspecies variety subvariety form subform}.each do |r|
      elements.push(d[r][0], "#{eo}#{d[r][1]}#{ec}") if d[r]
    end

    html = elements.flatten.compact.join(' ').gsub(/\(\s*\)/, '').gsub(/\(\s/, '(').gsub(/\s\)/, ')').squish.gsub(' [sic]', ec + ' [sic]' + eo).gsub(ec + ' ' + eo, ' ').gsub(eo + ec, '').gsub(eo + ' ', ' ' + eo)
    html
  end


  protected

  # @return [Array of TaxonNames, nil]
  #   return the component names for this combination prior to it being saved
  def protonyms_by_association
    APPLICABLE_RANKS.collect{|r| self.send(r)}.compact
  end

  # TODO: this is a TaxonName level validation, it doesn't belong here 
  def sv_year_of_publication_matches_source
    source_year = self.source.nomenclature_year if self.source
    if self.year_of_publication && source_year
      soft_validations.add(:year_of_publication, 'the asserted published date is not the same as provided by the source') if source_year != self.year_of_publication
    end
  end

  def sv_source_not_older_than_protonyms
    source_year = self.source.nomenclature_year if self.source
    target_year = earliest_protonym_year
    if source_year && target_year
      soft_validations.add(:base, 'the published date for the source is older than a name in the combination') if source_year < target_year
    end
  end

  def sv_year_of_publication_not_older_than_protonyms
    combination_year = self.year_of_publication
    target_year = earliest_protonym_year
    if combination_year && target_year
      soft_validations.add(:year_of_publication, 'the asserted published date is older than a name in the combination') if combination_year < target_year
    end
  end

  def sv_combination_duplicates
    duplicate = Combination.not_self(self).with_cached_html(self.cached_html)
#    duplicate = Combination.not_self(self).with_parent_id(self.parent_id).with_cached_html(self.cached_html)
    soft_validations.add(:base, 'Combination is a duplicate') unless duplicate.empty?
  end

  def set_parent
    names = self.protonyms 
    if names.count > 0
      self.parent = names.first.parent if names.first.parent
    end
  end

  def set_cached
    write_attribute(:cached, get_full_name) unless self.no_cached
  end

  def set_cached_html
    write_attribute(:cached_html, get_full_name_html) unless self.no_cached
  end

  # validations

  # The parent of a Combination is the parent of the highest ranked protonym in that combination 
  def parent_is_properly_set
    check = protonyms.first
    if self.parent && check && check.parent
      errors.add(:base, 'Parent is not highest ranked member') if  self.parent != check.parent  
    end
  end

  def at_least_two_protonyms_are_included
    c = protonyms.count

    if c == 0
      errors.add(:base, 'Combination includes no taxa, it is not valid')
    else
      rank = protonyms.last.rank_string

      if rank =~/Species/
        errors.add(:base, 'Combination includes only one taxon, it is not valid') if c < 2
      elsif rank =~/Genus/
        errors.add(:base, 'Combination includes more than two taxa, it is not valid') if c > 2
      else
        errors.add(:base, 'Combination includes more than one taxon, it is not valid') if c > 1
      end
    end
  end

  
  def validate_rank_class_class
    errors.add(:rank_class, 'Combination should not have rank') if !!self.rank_class
  end

end

Instance Method Details

- (Object) at_least_two_protonyms_are_included (protected)



330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
# File 'app/models/combination.rb', line 330

def at_least_two_protonyms_are_included
  c = protonyms.count

  if c == 0
    errors.add(:base, 'Combination includes no taxa, it is not valid')
  else
    rank = protonyms.last.rank_string

    if rank =~/Species/
      errors.add(:base, 'Combination includes only one taxon, it is not valid') if c < 2
    elsif rank =~/Genus/
      errors.add(:base, 'Combination includes more than two taxa, it is not valid') if c > 2
    else
      errors.add(:base, 'Combination includes more than one taxon, it is not valid') if c > 1
    end
  end
end

- (Object) combination_class_relationships(rank_string)

return [Array of TaxonNameRelationship]

classes that are applicable to this name, as deterimned by Rank


200
201
202
203
204
205
206
# File 'app/models/combination.rb', line 200

def combination_class_relationships(rank_string)
  relations = []
  TaxonNameRelationship::Combination.descendants.each do |r|
    relations.push(r) if r.valid_object_ranks.include?(rank_string)
  end
  relations
end

- (Object) combination_relationships_and_stubs(rank_string)



208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'app/models/combination.rb', line 208

def combination_relationships_and_stubs(rank_string)
  display_order = [
      :combination_genus, :combination_subgenus, :combination_species, :combination_subspecies, :combination_variety, :combination_form
  ]

  defined_relations = self.combination_relationships.all
  created_already = defined_relations.collect{|a| a.class}
  new_relations = []

  combination_class_relationships(rank_string).each do |r|
    new_relations.push( r.new(object_taxon_name: self) ) if !created_already.include?(r)
  end

  (new_relations + defined_relations).sort{|a,b|
    display_order.index(a.class.inverse_assignment_method) <=> display_order.index(b.class.inverse_assignment_method)
  }
end

- (Integer?) earliest_protonym_year

Returns the earliest year (nomenclature) that a component Protonym was published on

Returns:

  • (Integer, nil)

    the earliest year (nomenclature) that a component Protonym was published on



194
195
196
# File 'app/models/combination.rb', line 194

def earliest_protonym_year
  publication_years.sort.first
end

- (Hash) full_name_hash

Returns:

  • (Hash)


163
164
165
166
167
168
169
170
171
172
# File 'app/models/combination.rb', line 163

def full_name_hash
  gender = nil
  data   = {}
  protonyms_by_rank.each do |rank, name|
    gender = name.gender_name if rank == 'genus'
    method = "#{rank.gsub(/\s/, '_')}_name_elements"
    data[rank] = send(method, name, gender) if self.respond_to?(method)
  end
  data
end

- (Object) get_author_and_year



241
242
243
244
# File 'app/models/combination.rb', line 241

def get_author_and_year
  ay = iczn_author_and_year
  ay.blank? ? nil : ay
end

- (Object) get_full_name_html



246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
# File 'app/models/combination.rb', line 246

def get_full_name_html
  eo = '<i>'
  ec = '</i>'
  return "#{eo}#{verbatim_name}#{ec}".gsub(' f. ', ec + ' f. ' + eo).gsub(' var. ', ec + ' var. ' + eo) if !self.verbatim_name.nil?
  d = full_name_hash

  elements = []

  elements.push("#{eo}#{d['genus'][1]}#{ec}") if d['genus']
  elements.push ['(', %w{subgenus section subsection series subseries}.collect { |r| d[r] ? [d[r][0], "#{eo}#{d[r][1]}#{ec}"] : nil }, ')']
  elements.push ['(', eo, d['superspecies'][1], ec, ')'] if d['superspecies']

  %w{species subspecies variety subvariety form subform}.each do |r|
    elements.push(d[r][0], "#{eo}#{d[r][1]}#{ec}") if d[r]
  end

  html = elements.flatten.compact.join(' ').gsub(/\(\s*\)/, '').gsub(/\(\s/, '(').gsub(/\s\)/, ')').squish.gsub(' [sic]', ec + ' [sic]' + eo).gsub(ec + ' ' + eo, ' ').gsub(eo + ec, '').gsub(eo + ' ', ' ' + eo)
  html
end

- (Object) get_valid_taxon_name



235
236
237
238
239
# File 'app/models/combination.rb', line 235

def get_valid_taxon_name
  c = self.protonyms_by_rank
  return self if c.blank?
  c[c.keys.last].valid_taxon_name
end

- (Object) parent_is_properly_set (protected)

The parent of a Combination is the parent of the highest ranked protonym in that combination



323
324
325
326
327
328
# File 'app/models/combination.rb', line 323

def parent_is_properly_set
  check = protonyms.first
  if self.parent && check && check.parent
    errors.add(:base, 'Parent is not highest ranked member') if  self.parent != check.parent  
  end
end

- (Array of TaxonName) protonyms

Returns pre-ordered by rank

Returns:

  • (Array of TaxonName)

    pre-ordered by rank



153
154
155
156
157
158
159
# File 'app/models/combination.rb', line 153

def protonyms 
  if self.new_record?
    protonyms_by_association
  else
    self.combination_taxon_names.sort{|a,b| RANKS.index(a.rank_string) <=> RANKS.index(b.rank_string)}  # .ordered_by_rank
  end
end

- (Array of TaxonNames?) protonyms_by_association (protected)

Return the component names for this combination prior to it being saved

Returns:

  • (Array of TaxonNames, nil)

    return the component names for this combination prior to it being saved



271
272
273
# File 'app/models/combination.rb', line 271

def protonyms_by_association
  APPLICABLE_RANKS.collect{|r| self.send(r)}.compact
end

- (Array of TaxonNames?) protonyms_by_rank

Returns the component names for this combination prior to it being saved (used to return values prior to save)

Returns:

  • (Array of TaxonNames, nil)

    the component names for this combination prior to it being saved (used to return values prior to save)



176
177
178
179
180
181
182
183
184
# File 'app/models/combination.rb', line 176

def protonyms_by_rank
  result = {}
  APPLICABLE_RANKS.each do |rank|
    if protonym = self.send(rank)
      result[rank] = protonym
    end
  end
  result
end

- (Array of Integer) publication_years

Returns the collective years the protonyms were (nomenclaturaly) published on (ordered from genus to below)

Returns:

  • (Array of Integer)

    the collective years the protonyms were (nomenclaturaly) published on (ordered from genus to below)



188
189
190
# File 'app/models/combination.rb', line 188

def publication_years 
  description_years = protonyms.collect{|a| a.nomenclature_date ? a.nomenclature_date.year : nil}.compact
end

- (Object) set_cached (protected)



312
313
314
# File 'app/models/combination.rb', line 312

def set_cached
  write_attribute(:cached, get_full_name) unless self.no_cached
end

- (Object) set_cached_html (protected)



316
317
318
# File 'app/models/combination.rb', line 316

def set_cached_html
  write_attribute(:cached_html, get_full_name_html) unless self.no_cached
end

- (Object) set_cached_valid_taxon_name_id



226
227
228
229
230
231
232
233
# File 'app/models/combination.rb', line 226

def set_cached_valid_taxon_name_id
  begin
    TaxonName.transaction do
      self.update_column(:cached_valid_taxon_name_id, self.get_valid_taxon_name.id)
    end
  rescue
  end
end

- (Object) set_parent (protected)



305
306
307
308
309
310
# File 'app/models/combination.rb', line 305

def set_parent
  names = self.protonyms 
  if names.count > 0
    self.parent = names.first.parent if names.first.parent
  end
end

- (Object) sv_combination_duplicates (protected)



299
300
301
302
303
# File 'app/models/combination.rb', line 299

def sv_combination_duplicates
  duplicate = Combination.not_self(self).with_cached_html(self.cached_html)
#    duplicate = Combination.not_self(self).with_parent_id(self.parent_id).with_cached_html(self.cached_html)
  soft_validations.add(:base, 'Combination is a duplicate') unless duplicate.empty?
end

- (Object) sv_source_not_older_than_protonyms (protected)



283
284
285
286
287
288
289
# File 'app/models/combination.rb', line 283

def sv_source_not_older_than_protonyms
  source_year = self.source.nomenclature_year if self.source
  target_year = earliest_protonym_year
  if source_year && target_year
    soft_validations.add(:base, 'the published date for the source is older than a name in the combination') if source_year < target_year
  end
end

- (Object) sv_year_of_publication_matches_source (protected)

TODO: this is a TaxonName level validation, it doesn't belong here



276
277
278
279
280
281
# File 'app/models/combination.rb', line 276

def sv_year_of_publication_matches_source
  source_year = self.source.nomenclature_year if self.source
  if self.year_of_publication && source_year
    soft_validations.add(:year_of_publication, 'the asserted published date is not the same as provided by the source') if source_year != self.year_of_publication
  end
end

- (Object) sv_year_of_publication_not_older_than_protonyms (protected)



291
292
293
294
295
296
297
# File 'app/models/combination.rb', line 291

def sv_year_of_publication_not_older_than_protonyms
  combination_year = self.year_of_publication
  target_year = earliest_protonym_year
  if combination_year && target_year
    soft_validations.add(:year_of_publication, 'the asserted published date is older than a name in the combination') if combination_year < target_year
  end
end

- (Object) validate_rank_class_class (protected)



349
350
351
# File 'app/models/combination.rb', line 349

def validate_rank_class_class
  errors.add(:rank_class, 'Combination should not have rank') if !!self.rank_class
end