001/*
002 * Licensed under the Apache License, Version 2.0 (the "License");
003 * you may not use this file except in compliance with the License.
004 * You may obtain a copy of the License at
005 *
006 *     http://www.apache.org/licenses/LICENSE-2.0
007 *
008 * Unless required by applicable law or agreed to in writing, software
009 * distributed under the License is distributed on an "AS IS" BASIS,
010 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
011 * See the License for the specific language governing permissions and
012 * limitations under the License.
013 */
014package org.gbif.api.model.checklistbank;
015
016import org.gbif.api.jackson.RankSerde;
017import org.gbif.api.util.UnicodeUtils;
018import org.gbif.api.vocabulary.NamePart;
019import org.gbif.api.vocabulary.NameType;
020import org.gbif.api.vocabulary.Rank;
021
022import java.util.Objects;
023
024import org.apache.commons.lang3.StringUtils;
025
026import com.fasterxml.jackson.annotation.JsonIgnore;
027import com.fasterxml.jackson.annotation.JsonInclude;
028import com.fasterxml.jackson.annotation.JsonProperty;
029import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
030import com.fasterxml.jackson.databind.annotation.JsonSerialize;
031
032import io.swagger.v3.oas.annotations.media.Schema;
033
034/**
035 * A container of a taxon name that is atomised into its relevant, separate parts.
036 * <br>
037 * Hybrid formulas that consist of multiple genera, binomials or species
038 * epithets are relatively poorly represented. A simple boolean flag indicates a
039 * hybrid formula, e.g. Polygala vulgaris × Polygala epinema but not named
040 * hybrids that are have an × or notho as part of a well-formed
041 * mono/bi/trinomial e.g. ×Polygala vulgaris. In the case of hybrid formulas,
042 * i.e. isHybrid=true, the first name in the formula is tried to be kept, at
043 * least the genus.
044 * <p/>
045 * A container of a taxon name that is only atomised into three name parts maximum plus rank and a notho property
046 * indicating the name part of named hybrids that is considered to be the hybrid. No authorship is kept. For subgenera
047 * we don't use parentheses to indicate the subgenus, but use explicit rank markers instead.
048 */
049@SuppressWarnings("unused")
050public class ParsedName {
051
052  public static final Character HYBRID_MARKER = '×';
053  private static final String HYBRID_MARKER_STR = HYBRID_MARKER.toString();
054
055  private Integer key;
056  private String scientificName;
057  @JsonProperty("rankMarker")
058  @JsonInclude(JsonInclude.Include.NON_NULL)
059  @JsonSerialize(using = RankSerde.RankJsonSerializer.class)
060  @JsonDeserialize(using = RankSerde.RankJsonDeserializer.class)
061  private Rank rank;
062  private NameType type;
063  private String genusOrAbove;
064  private String infraGeneric;
065  private String specificEpithet;
066  private String infraSpecificEpithet;
067  private String cultivarEpithet;
068  private String strain;
069  private NamePart notho;
070  private String authorship;
071  private String year;
072  private String bracketAuthorship;
073  private String bracketYear;
074  private String sensu;
075  private boolean parsed = true;
076  private boolean parsedPartially = false;
077
078  /**
079   * nomenclatural status note.
080   */
081  private String nomStatus;
082  private String remarks;
083
084  public ParsedName() {
085  }
086
087  public ParsedName(
088    NameType type,
089    String genusOrAbove,
090    String infraGeneric,
091    String specificEpithet,
092    String infraSpecificEpithet,
093    NamePart notho,
094    Rank rank,
095    String authorship,
096    String year,
097    String bracketAuthorship,
098    String bracketYear,
099    String cultivarEpithet,
100    String strain,
101    String sensu,
102    String nomStatus,
103    String remarks
104  ) {
105    this.type = type;
106    this.genusOrAbove = genusOrAbove;
107    this.infraGeneric = infraGeneric;
108    this.specificEpithet = specificEpithet;
109    this.infraSpecificEpithet = infraSpecificEpithet;
110    this.notho = notho;
111    this.rank = rank;
112    this.authorship = authorship;
113    this.year = year;
114    this.bracketAuthorship = bracketAuthorship;
115    this.bracketYear = bracketYear;
116    this.cultivarEpithet = cultivarEpithet;
117    this.strain = strain;
118    this.sensu = sensu;
119    this.nomStatus = nomStatus;
120    this.remarks = remarks;
121  }
122
123  /**
124   * The original author of this name, e.g. basionym or recombination author
125   */
126  @Schema(description = "The original author of this name, e.g. basionym or recombination author.")
127  public String getAuthorship() {
128    return authorship;
129  }
130
131  public void setAuthorship(String authorship) {
132    this.authorship = authorship;
133  }
134
135  /**
136   * The authorship of the original name, i.e. basionym, given in brackets.
137   */
138  @Schema(description = "The authorship of the original name, i.e. basionym, given in brackets.")
139  public String getBracketAuthorship() {
140    return bracketAuthorship;
141  }
142
143  public void setBracketAuthorship(String bracketAuthorship) {
144    this.bracketAuthorship = bracketAuthorship;
145  }
146
147  /**
148   * The code relevant year of publication of the original name, i.e. basionym, given in brackets.
149   */
150  @Schema(description = "The code relevant year of publication of the original name, i.e. basionym, given in brackets.")
151  public String getBracketYear() {
152    return bracketYear;
153  }
154
155  public void setBracketYear(String bracketYear) {
156    this.bracketYear = bracketYear;
157  }
158
159  /**
160   * The cultivar, cultivar group or grex part of a cultivated plant name.
161   * If given the name should be of type NameType.CULTIVAR
162   */
163  @Schema(description = "The cultivar, cultivar group or grex part of a cultivated plant name.\n\n" +
164    "If given the name should be of type CULTIVAR")
165  public String getCultivarEpithet() {
166    return cultivarEpithet;
167  }
168
169  public void setCultivarEpithet(String cultivarEpithet) {
170    this.cultivarEpithet = cultivarEpithet;
171  }
172
173  /**
174   * The strain or isolate name. Usually a capital collection code string followed by an accession number.
175   * See <a href="http://www.bacterio.net/-collections.html">List of culture collection codes</a>
176   * If given the name should be of type NameType.STRAIN
177   */
178  @Schema(description = "The strain or isolate name. Usually a capital collection code string followed by an accession number.\n\n" +
179    "See [List of culture collection codes](http://www.bacterio.net/-collections.html).  If given the name should be of type STRAIN.")
180  public String getStrain() {
181    return strain;
182  }
183
184  public void setStrain(String strain) {
185    this.strain = strain;
186  }
187
188  /**
189   * The genus part of a bi/trinomial or the monomial in case of names of higher ranks
190   */
191  @Schema(description = "The genus part of a bi/trinomial or the monomial in case of names of higher ranks.")
192  public String getGenusOrAbove() {
193    return genusOrAbove;
194  }
195
196  /**
197   * The infrageneric part of a name, often given in parentheses between genus and species epithet, e.g. for a subgenus
198   */
199  @Schema(description = "The infrageneric part of a name, often given in parentheses between genus and species epithet, " +
200    "e.g. for a subgenus")
201  public String getInfraGeneric() {
202    return infraGeneric;
203  }
204
205  @Schema(description = "The infraspecific part of a name.")
206  public String getInfraSpecificEpithet() {
207    return infraSpecificEpithet;
208  }
209
210  /**
211   * Any nomenclatoral remarks given in this name, e.g. nom. illeg.
212   */
213  @Schema(description = "Any nomenclatoral remarks given in this name, e.g. nom. illeg.")
214  public String getNomStatus() {
215    return nomStatus;
216  }
217
218  public void setNomStatus(String nomStatus) {
219    this.nomStatus = nomStatus;
220  }
221
222  /**
223   * For hybrid names notho indicates which part of the name is considered a hybrid,
224   * i.e. genus, species or infraspecific epithet.
225   */
226  @Schema(description = "For hybrid names notho indicates which part of the name is considered a hybrid, " +
227    "i.e. genus, species or infraspecific epithet.")
228  public NamePart getNotho() {
229    return notho;
230  }
231
232  public void setNotho(NamePart notho) {
233    this.notho = notho;
234  }
235
236  /**
237   * Any further remarks found
238   */
239  @Schema(description = "Any further remarks found.")
240  public String getRemarks() {
241    return remarks;
242  }
243
244  public void setRemarks(String remarks) {
245    this.remarks = remarks;
246  }
247
248  /**
249   * Taxon concept references as part of the name,
250   * e.g. "MSW2005" for Gorilla gorilla (Savage, 1847) sec. MSW2005
251   */
252  @Schema(description = "Taxon concept references as part of the name, e.g. “MSW2006” for *Gorilla gorilla* " +
253    "(Savage, 1847) sec. MSW2005")
254  public String getSensu() {
255    return sensu;
256  }
257
258  public void setSensu(String sensu) {
259    this.sensu = sensu;
260  }
261
262  @Schema(description = "The specific epithet of the name")
263  public String getSpecificEpithet() {
264    return specificEpithet;
265  }
266
267  /**
268   * @return the terminal epithet, infraspecific epithet if existing, the species epithet or null
269   */
270  @Schema(description = "The terminal epithet, infraspecific epithet if existing, the species epithet or null")
271  @JsonIgnore
272  public String getTerminalEpithet() {
273    return infraSpecificEpithet == null ? specificEpithet : infraSpecificEpithet;
274  }
275
276  /**
277   * A coarse classification of names helping to deal with different syntactical name string structures.
278   */
279  @Schema(description = "A coarse classification of names helping to deal with different syntactical name string structures.")
280  public NameType getType() {
281    return type;
282  }
283
284  public void setType(NameType type) {
285    this.type = type;
286  }
287
288  /**
289   * The year of publication as given in the authorship.
290   */
291  @Schema(description = "The year of publication as given in the authorship.")
292  public String getYear() {
293    return year;
294  }
295
296  public void setYear(String year) {
297    this.year = year;
298  }
299
300  @JsonIgnore
301  public boolean hasAuthorship() {
302    return authorship != null || year != null || bracketAuthorship != null || bracketYear != null;
303  }
304
305  @Schema(description = "The taxonomic rank of the name.")
306  public void setRank(Rank rank) {
307    this.rank = rank;
308  }
309
310  @Schema(description = "The key for this parsed name object.")
311  public Integer getKey() {
312    return key;
313  }
314
315  public void setKey(Integer key) {
316    this.key = key;
317  }
318
319  /**
320   * The exact verbatim, full scientific name as given before parsing.
321   */
322  @Schema(description = "The exact verbatim, full scientific name as given before parsing.")
323  public String getScientificName() {
324    return scientificName;
325  }
326
327  public void setScientificName(String scientificName) {
328    this.scientificName = scientificName;
329  }
330
331  /**
332   * The GBIF name parser cannot always parse the entire name.
333   * This is often the case when taxonomic, nomenclatural or identification notes are added to the end of the name.
334   * In such cases when the name was only partially parsed (NameUsageIssue.PARTIALLY_PARSABLE)
335   * this flag should be true.
336   *
337   * If the name was not parsed at all this will be false.
338   *
339   * @return true if name was only partially parsed, false otherwise
340   */
341  @Schema(description = "The GBIF name parser cannot always parse the entire name.\n\n" +
342    "This is often the case when taxonomic, nomenclatural or identification notes are added to the end of the name. " +
343    "In such cases when the name was only partially parsed (NameUsageIssue.PARTIALLY_PARSABLE) " +
344    "this flag should be true.\n\n" +
345    "If the name was not parsed at all this will be false.")
346  public boolean isParsedPartially() {
347    return parsedPartially;
348  }
349
350  public void setParsedPartially(boolean parsedPartially) {
351    this.parsedPartially = parsedPartially;
352  }
353
354  /**
355   * A flag indicating if a name could not be parsed at all.
356   * If false, only the scientific name, rank and potentially the name type is given.
357   */
358  @Schema(description = "A flag indicating if a name could not be parsed at all.\n\n" +
359    "If false, only the scientific name, rank and potentially the name type is given.")
360  public boolean isParsed() {
361    return parsed;
362  }
363
364  public void setParsed(boolean parsed) {
365    this.parsed = parsed;
366  }
367
368  /**
369   * @return The full concatenated authorship or null if it is a hybrid
370   */
371  public String authorshipComplete() {
372    StringBuilder sb = new StringBuilder();
373    appendAuthorship(sb);
374    return sb.toString().trim();
375  }
376
377  /**
378   * build a name controlling all available flags for name parts to be included in the resulting name.
379   *
380   * @param hybridMarker    include the hybrid marker with the name if existing
381   * @param rankMarker      include the infraspecific or infrageneric rank marker with the name if existing
382   * @param authorship      include the names authorship (authorteam and year)
383   * @param infrageneric include the infrageneric name in brackets for species or infraspecies
384   * @param genusForInfrageneric include the genus name in front of an infrageneric name (not a species)
385   * @param abbreviateGenus if true abreviate the genus with its first character
386   * @param decomposition   decompose unicode ligatures into their corresponding ascii ones, e.g. æ beomes ae
387   * @param asciiOnly       transform unicode letters into their corresponding ascii ones, e.g. ø beomes o and ü u
388   * @param showIndet       if true include the rank marker for incomplete determinations, for example Puma spec.
389   * @param nomNote         include nomenclatural notes
390   * @param remarks         include informal remarks
391   */
392  public String buildName(
393    boolean hybridMarker,
394    boolean rankMarker,
395    boolean authorship,
396    boolean infrageneric,
397    boolean genusForInfrageneric,
398    boolean abbreviateGenus,
399    boolean decomposition,
400    boolean asciiOnly,
401    boolean showIndet,
402    boolean nomNote,
403    boolean remarks,
404    boolean showSensu,
405    boolean showCultivar,
406    boolean showStrain
407  ) {
408    StringBuilder sb = new StringBuilder();
409
410    if (NameType.CANDIDATUS == type) {
411      sb.append("Candidatus ");
412    }
413
414    if (genusOrAbove != null && (genusForInfrageneric || infraGeneric == null || specificEpithet != null)) {
415      if (hybridMarker && NamePart.GENERIC == notho) {
416        sb.append(HYBRID_MARKER);
417      }
418      if (abbreviateGenus) {
419        sb.append(genusOrAbove.charAt(0)).append('.');
420      } else {
421        sb.append(genusOrAbove);
422      }
423    }
424    if (specificEpithet == null) {
425      if (Rank.SPECIES == rank) {
426        // no species epitheton given, but rank=species. Indetermined species!
427        if (showIndet(showIndet)) {
428          sb.append(" spec.");
429        }
430      } else if (rank != null && rank.isInfraspecific()) {
431        // no species epitheton given, but rank below species. Indetermined!
432        if (showIndet(showIndet)) {
433          sb.append(' ');
434          sb.append(rank.getMarker());
435        }
436      } else if (infraGeneric != null) {
437        // this is the terminal name part - always show it!
438        if (rankMarker && rank != null) {
439          // If we know the rank we use explicit rank markers
440          // this is how botanical infrageneric names are formed, see http://www.iapt-taxon.org/nomen/main.php?page=art21
441          sb.append(' ');
442          appendRankMarker(sb, rank);
443          sb.append(infraGeneric);
444
445        } else {
446          if (genusForInfrageneric && genusOrAbove != null) {
447            // if we have shown the genus already and we do not know the rank we use parenthesis to indicate an infrageneric
448            sb.append(" (")
449            .append(infraGeneric)
450            .append(")");
451          } else {
452            // no genus shown yet, just show the plain infrageneric name
453            sb.append(infraGeneric);
454          }
455        }
456      }
457      // genus/infrageneric authorship
458      if (authorship) {
459        appendAuthorship(sb);
460      }
461    } else {
462      if (infrageneric && infraGeneric != null && (rank == null || rank == Rank.GENUS)) {
463        // only show subgenus if requested
464        sb.append(" (");
465        sb.append(infraGeneric);
466        sb.append(')');
467      }
468
469      // species part
470      sb.append(' ');
471      if (hybridMarker && NamePart.SPECIFIC == notho) {
472        sb.append(HYBRID_MARKER);
473      }
474      String epi = specificEpithet.replaceAll("[ _-]", "-");
475      sb.append(epi);
476
477      if (infraSpecificEpithet == null) {
478        // Indetermined? Only show indet cultivar marker if no cultivar epithet exists
479        if (showIndet(showIndet) && rank != null && rank.isInfraspecific() && (Rank.CULTIVAR != rank || cultivarEpithet == null)) {
480          // no infraspecific epitheton given, but rank below species. Indetermined!
481          sb.append(' ');
482          sb.append(rank.getMarker());
483        }
484
485        // species authorship
486        if (authorship) {
487          appendAuthorship(sb);
488        }
489      } else {
490        // infraspecific part
491        sb.append(' ');
492        if (hybridMarker && NamePart.INFRASPECIFIC == notho) {
493          if (rankMarker) {
494            sb.append("notho");
495          } else {
496            sb.append(HYBRID_MARKER);
497          }
498        }
499        if (rankMarker) {
500          appendRankMarker(sb, rank);
501        }
502        epi = infraSpecificEpithet.replaceAll("[ _-]", "-");
503        sb.append(epi);
504        // non autonym authorship ?
505        if (authorship && !isAutonym()) {
506          appendAuthorship(sb);
507        }
508      }
509    }
510
511    // add cultivar name
512    if (showStrain && strain != null) {
513      sb.append(" ");
514      sb.append(strain);
515    }
516
517    // add cultivar name
518    if (showCultivar && cultivarEpithet != null) {
519      sb.append(" '");
520      sb.append(cultivarEpithet);
521      sb.append("'");
522    }
523
524    // add sensu/sec reference
525    if (showSensu && sensu != null) {
526      sb.append(" ");
527      sb.append(sensu);
528    }
529
530    // add nom status
531    if (nomNote && nomStatus != null) {
532      sb.append(", ");
533      sb.append(nomStatus);
534    }
535
536    // add remarks
537    if (remarks && this.remarks != null) {
538      sb.append(" [");
539      sb.append(this.remarks);
540      sb.append("]");
541    }
542
543    String name = sb.toString().trim();
544    if (decomposition) {
545      name = UnicodeUtils.decompose(name);
546    }
547    if (asciiOnly) {
548      name = UnicodeUtils.ascii(name);
549    }
550
551    return StringUtils.trimToNull(name);
552  }
553
554  private boolean showIndet(boolean showIndet) {
555    return showIndet && (type == null || type.isParsable());
556  }
557
558  private void appendRankMarker(StringBuilder sb, Rank rank) {
559    if (rank != null && !rank.isUncomparable() && rank.getMarker() != null) {
560      sb.append(rank.getMarker());
561      sb.append(' ');
562    }
563  }
564
565  private void appendAuthorship(StringBuilder sb) {
566    if (bracketAuthorship == null) {
567      if (bracketYear != null) {
568        sb.append(" (");
569        sb.append(bracketYear);
570        sb.append(")");
571      }
572    } else {
573      sb.append(" (");
574      sb.append(bracketAuthorship);
575      if (bracketYear != null) {
576        sb.append(", ");
577        sb.append(bracketYear);
578      }
579      sb.append(")");
580    }
581    if (authorship != null) {
582      sb.append(" ").append(authorship);
583    }
584    if (year != null) {
585      sb.append(", ");
586      sb.append(year);
587    }
588  }
589
590  /**
591   * The canonical name sensu strictu with nothing else but 3 name parts at max (genus, species, infraspecific). No
592   * rank or hybrid markers and no authorship, cultivar or strain information.
593   * Infrageneric names are represented without a leading genus.
594   * Unicode characters will be replaced by their matching ASCII characters.
595   * <p/>
596   * For example:
597   * Abies alba
598   * Abies alba alpina
599   * Abies Bracteata
600   * Heucherella tiarelloides
601   *
602   * @return the 1, 2 or 3 parted name as a single string
603   */
604  @Schema(description = "The canonical name *sensu strictu* with nothing else but three name parts at most (genus, " +
605    "species, infraspecific).\n\n" +
606    "No rank or hybrid markers and no authorship, cultivar or strain information.\n\n" +
607    "Infrageneric names are represented without a leading genus.\n\n" +
608    "Unicode characters are replaced by their matching ASCII characters.")
609  @JsonProperty
610  public String canonicalName() {
611    return buildName(false, false, false, false, false, false, true, true, true, false, false, false, false, false);
612  }
613
614  /**
615   * The code-compliant, canonical name with three name parts at max (genus, species, infraspecific), a rank marker for
616   * infraspecific names and cultivar or strain epithets. The canonical name can be a 1, 2 or 3 parted name, but does
617   * not include any informal notes or authorships. Notho taxa will have the hybrid marker.
618   * Unicode characters will be replaced by their matching ASCII characters.
619   * <p/>
620   * For example:
621   * Abies alba
622   * Abies alba subsp. alpina
623   * Abies sect. Bracteata
624   * ×Heucherella tiarelloides
625   *
626   * @return the 1,2 or 3 parted name as a single string
627   */
628  @Schema(description = "The code-compliant, canonical name with three name parts at max (genus, species, " +
629    "infraspecific), a rank marker for infraspecific names and cultivar or strain epithets. The canonical name can " +
630    "be a 1, 2 or 3 parted name, but does not include any informal notes or authorships. Notho taxa will have the " +
631    "hybrid marker.\n\n" +
632    "Unicode characters will be replaced by their matching ASCII characters.")
633  @JsonProperty
634  public String canonicalNameWithMarker() {
635    return buildName(true, true, false, false, false, false, true, true, true, false, false, false, true, true);
636  }
637
638  /**
639   * The code-compliant, canonical name with rank and hybrid marker, authorship and cultivar or strain name included.
640   * Informal or nomenclatoral notes, concept references, subgenus and non-terminal authorships are removed.
641   * @return the 1,2 or 3 parted name as a single string
642   */
643  @Schema(description = "The code-compliant, canonical name with rank and hybrid marker, authorship and cultivar " +
644    "or strain name included. Informal or nomenclatoral notes, concept references, subgenus and non-terminal " +
645    "authorships are removed.")
646  @JsonProperty
647  public String canonicalNameComplete() {
648    return buildName(true, true, true, false, true, false, true, false, true, false, false, false, true, true);
649  }
650
651  /**
652   * @return the species binomial if this parsed name is a species or below. Or null in case its superspecific
653   */
654  public String canonicalSpeciesName() {
655    if (genusOrAbove != null && specificEpithet != null) {
656      return genusOrAbove + " " + specificEpithet;
657    }
658    return null;
659  }
660
661  /**
662   * @return the name with all details that exist.
663   */
664  public String fullName() {
665    return buildName(true, true, true, true, true, false, false, false, true, true, true, true, true, true);
666  }
667
668  @JsonIgnore
669  public Integer getBracketYearInt() {
670    try {
671      return Integer.parseInt(bracketYear);
672    } catch (NumberFormatException e) {
673      return null;
674    }
675  }
676
677  /**
678   * @return rank as enumeration or null
679   */
680  public Rank getRank() {
681    return rank;
682  }
683
684  @JsonIgnore
685  public Integer getYearInt() {
686    try {
687      return Integer.parseInt(year);
688    } catch (NumberFormatException e) {
689      return null;
690    }
691  }
692
693  @JsonIgnore
694  public boolean isAutonym() {
695    return specificEpithet != null && specificEpithet.equals(infraSpecificEpithet);
696  }
697
698  @JsonIgnore
699  public boolean isBinomial() {
700    return genusOrAbove != null && specificEpithet != null;
701  }
702
703  @JsonIgnore
704  public boolean isHybridFormula() {
705    return NameType.HYBRID == type;
706  }
707
708  /**
709   * @return true for names with an infraspecifc rank but missing lowest name part. E.g. Coccyzuz americanus ssp. or
710   *         Asteraceae
711   *         spec. but not Maxillaria sect. Acaules
712   */
713  @JsonIgnore
714  public boolean isIndetermined() {
715    return rank != null && isParsed() && isParsableType() && rank.notOtherOrUnknown() && (
716           (rank.isInfragenericStrictly() && infraGeneric == null)
717        || (rank.isSpeciesAggregateOrBelow() && specificEpithet == null)
718        || (rank.isInfraspecific() && infraSpecificEpithet == null)
719    );
720  }
721
722  @JsonIgnore
723  public boolean isParsableType() {
724    return type != null && type.isParsable();
725  }
726
727  @JsonIgnore
728  public boolean isQualified() {
729    return (authorship != null && !authorship.isEmpty())
730           || (year != null && !year.isEmpty())
731           || (bracketAuthorship != null && !bracketAuthorship.isEmpty())
732           || (bracketYear != null && !bracketYear.isEmpty());
733  }
734
735  /**
736   * @return true if a bracket authorship is given, indicating that the name has been subsequently recombined.
737   */
738  @JsonIgnore
739  public boolean isRecombination() {
740    return (!StringUtils.isBlank(bracketAuthorship) || !StringUtils.isBlank(bracketYear));
741  }
742
743
744  public void setGenusOrAbove(String genusOrAbove) {
745    if (genusOrAbove != null && genusOrAbove.startsWith(HYBRID_MARKER.toString())) {
746      this.genusOrAbove = genusOrAbove.substring(1);
747      notho = NamePart.GENERIC;
748    } else {
749      this.genusOrAbove = genusOrAbove;
750    }
751  }
752
753  public void setHybridFormula(boolean hybrid) {
754    if (hybrid) {
755      type = NameType.HYBRID;
756    } else if (NameType.HYBRID == type) {
757      type = null;
758    }
759  }
760
761  public void setInfraGeneric(String infraGeneric) {
762    if (infraGeneric != null && infraGeneric.startsWith(HYBRID_MARKER_STR)) {
763      this.infraGeneric = infraGeneric.substring(1);
764      notho = NamePart.INFRAGENERIC;
765    } else {
766      this.infraGeneric = infraGeneric;
767    }
768  }
769
770  public void setInfraSpecificEpithet(String infraSpecies) {
771    if (infraSpecies != null && infraSpecies.startsWith(HYBRID_MARKER_STR)) {
772      this.infraSpecificEpithet = infraSpecies.substring(1);
773      this.notho = NamePart.INFRASPECIFIC;
774    } else {
775      this.infraSpecificEpithet = infraSpecies;
776    }
777  }
778
779  public void setSpecificEpithet(String species) {
780    if (species != null && species.startsWith(HYBRID_MARKER_STR)) {
781      specificEpithet = species.substring(1);
782      notho = NamePart.SPECIFIC;
783    } else {
784      specificEpithet = species;
785    }
786  }
787
788  @Override
789  public boolean equals(Object o) {
790    if (this == o) {
791      return true;
792    }
793    if (o == null || getClass() != o.getClass()) {
794      return false;
795    }
796    ParsedName that = (ParsedName) o;
797    return Objects.equals(key, that.key) &&
798      Objects.equals(scientificName, that.scientificName) &&
799      rank == that.rank &&
800      type == that.type &&
801      Objects.equals(genusOrAbove, that.genusOrAbove) &&
802      Objects.equals(infraGeneric, that.infraGeneric) &&
803      Objects.equals(specificEpithet, that.specificEpithet) &&
804      Objects.equals(infraSpecificEpithet, that.infraSpecificEpithet) &&
805      Objects.equals(cultivarEpithet, that.cultivarEpithet) &&
806      Objects.equals(strain, that.strain) &&
807      Objects.equals(authorship, that.authorship) &&
808      Objects.equals(year, that.year) &&
809      Objects.equals(bracketAuthorship, that.bracketAuthorship) &&
810      Objects.equals(bracketYear, that.bracketYear);
811  }
812
813  @Override
814  public int hashCode() {
815    return Objects
816      .hash(key, scientificName, rank, type, genusOrAbove, infraGeneric, specificEpithet,
817        infraSpecificEpithet, cultivarEpithet, strain, authorship, year, bracketAuthorship,
818        bracketYear);
819  }
820
821  @Override
822  public String toString() {
823    StringBuilder sb = new StringBuilder();
824    sb.append(scientificName);
825    if (key != null) {
826      sb.append(" [");
827      sb.append(key);
828      sb.append("]");
829    }
830    if (genusOrAbove != null) {
831      sb.append(" G:").append(genusOrAbove);
832    }
833    if (infraGeneric != null) {
834      sb.append(" IG:").append(infraGeneric);
835    }
836    if (specificEpithet != null) {
837      sb.append(" S:").append(specificEpithet);
838    }
839    if (rank != null) {
840      sb.append(" R:").append(rank);
841    }
842    if (infraSpecificEpithet != null) {
843      sb.append(" IS:").append(infraSpecificEpithet);
844    }
845    if (cultivarEpithet != null) {
846      sb.append(" CV:").append(cultivarEpithet);
847    }
848    if (strain != null) {
849      sb.append(" STR:").append(strain);
850    }
851    if (authorship != null) {
852      sb.append(" A:").append(authorship);
853    }
854    if (year != null) {
855      sb.append(" Y:").append(year);
856    }
857    if (bracketAuthorship != null) {
858      sb.append(" BA:").append(bracketAuthorship);
859    }
860    if (bracketYear != null) {
861      sb.append(" BY:").append(bracketYear);
862    }
863    if (type != null) {
864      sb.append(" [");
865      sb.append(type);
866      sb.append("]");
867    }
868    return isHybridFormula() ? " [hybrid]" : sb.toString();
869  }
870}