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