Module: Utilities::AuthorNames

Defined in:
lib/utilities/author_names.rb

Overview

Author name strings: split into surnames, and folded for comparison.

Usage:

Utilities::AuthorNames.surnames('Ritzi, C.M. & J.O. Whitaker, Jr.')
# => ['Ritzi', 'Whitaker']
Utilities::AuthorNames.fold('Creão-Duarte') # => 'creaoduarte'
Utilities::AuthorNames.authorship_key('(Walker, 1851)')
# => { surnames: ['walker'], year: 1851, parenthesized: true }

Constant Summary collapse

AUTHOR_SEPARATOR_PATTERN =
/\s*(?:&|;|\band\b|\bund\b|\bet\b)\s*/i
INITIALS_PATTERN =

R, g., R., C.M., F.G.A.M., J.-P., J-M., Y-L, Ch., KB.

/\A(?:
  [[:alpha:]](?:-[[:upper:]])*\.*|
  (?:[[:upper:]][[:lower:]]?\.-?)+\.*|
  [[:upper:]]{2}\.
)\z/x
NAME_SUFFIX_PATTERN =
/\A(?:jr|sr|ii|iii|iv)\.?\z/i
YEAR_PATTERN =
/\b(1[5-9]\d{2}|20\d{2})\b/

Class Method Summary collapse

Class Method Details

.authorship_key(authorship) ⇒ Hash?

The comparison key of a taxon name's authorship string, e.g. TaxonWorks #cached_author_year or CoL's #authorship:

* surnames - folded (.fold), in order
* year - the name's own: a "non Author, year" (homonym) suffix is
dropped first
* parenthesized - sensu ICZN;

nil when either surnames or year can't be found (e.g. "Lam.") - an incomplete key compares with nothing.

Parameters:

  • authorship (String, nil) —

    e.g. "(Walker, 1851)", "Melichar, 1898 non Walker, 1851"

Returns:

  • (Hash, nil) —

    { surnames: Array, year: Integer, parenthesized: Boolean }



58
59
60
61
62
63
64
65
66
67
# File 'lib/utilities/author_names.rb', line 58

def self.authorship_key(authorship)
  s = authorship.to_s.sub(/\s+non\s.*\z/i, '').squish
  year = s.scan(YEAR_PATTERN).last&.first&.to_i
  surnames = surnames(s.delete('()').gsub(YEAR_PATTERN, ''))
    .map { |surname| fold(surname) }
    .reject(&:empty?)
  return nil if year.nil? || surnames.empty?

  { surnames:, year:, parenthesized: s.start_with?('(') }
end

.fold(string) ⇒ String

Returns for comparing names: no diacritics, case, spaces, or punctuation.

Returns:

  • (String) —

    for comparing names: no diacritics, case, spaces, or punctuation



39
40
41
# File 'lib/utilities/author_names.rb', line 39

def self.fold(string)
  I18n.transliterate(string.to_s).downcase.gsub(/[^a-z]/, '')
end

.same_authorship?(a, b, parentheses: false) ⇒ Boolean

Returns false when either key is nil.

Parameters:

  • a (Hash, nil) —

    an .authorship_key

  • b (Hash, nil) —

    an .authorship_key

  • parentheses (Boolean) (defaults to: false) —

    whether parenthesized must agree too

Returns:

  • (Boolean) —

    false when either key is nil



73
74
75
76
77
78
# File 'lib/utilities/author_names.rb', line 73

def self.same_authorship?(a, b, parentheses: false)
  return false if a.nil? || b.nil?

  a[:surnames] == b[:surnames] && a[:year] == b[:year] &&
    (!parentheses || a[:parenthesized] == b[:parenthesized])
end

.surname_from_token(token) ⇒ Object (private)



80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# File 'lib/utilities/author_names.rb', line 80

def self.surname_from_token(token)
  words = token
    .sub(/\A[^[:alpha:]]+/, '') # remove leading junk
    # The second pattern here matches the position between '.' and any letter,
    # like 'KB.Agwanda' (uncommon).
    .split(/\s+|(?<=\.)(?=[[:alpha:]])/)
    .map(&:strip)
    .reject { |w|
      !w.match?(/[[:alpha:]]/) || w.match?(INITIALS_PATTERN) ||
        w.match?(NAME_SUFFIX_PATTERN)
    }

  name = words.join(' ').sub(/[.\s]+\z/, '')
  name.presence
end

.surnames(author_string) ⇒ Array<String>

Returns surnames in author order, initials and name suffixes removed.

Returns:

  • (Array<String>) —

    surnames in author order, initials and name suffixes removed



25
26
27
28
29
30
31
32
33
34
35
# File 'lib/utilities/author_names.rb', line 25

def self.surnames(author_string)
  s = author_string.to_s
    .gsub(/<[^>]*>/, ' ') # remove <...>
    .gsub(/\([^)]*\)/, ' ') # remove (...)
    .gsub(/\bet\.?\s*al\b\.?/i, ' ') # remove 'et al'
    .gsub(%r{/\s*\d{4}\b}, ' ') # remove years

  s.split(AUTHOR_SEPARATOR_PATTERN).flat_map { |chunk|
    chunk.split(',').filter_map { |token| surname_from_token(token) }
  }
end