Here is a plant name, formatted the way most websites do it:
Hosta 'Blue Angel'
And here it is formatted correctly:
Hosta 'Blue Angel'
The difference is small, but it's a rule. Botanical names follow two naming codes - the ICN for wild plants and the ICNCP for cultivated ones - and they are specific about typography:
- Italic: the genus, the species epithet and infraspecific epithets. Pinus mugo subsp. uncinata.
-
Roman (not italic): rank markers (
subsp.,var.,f.), author citations, the hybrid sign×, cultivar groups - and cultivar names, which go in single quotes.
So Hydrangea ×macrophylla subsp. serrata 'Bluebird' has three italic words and three roman parts. Wrapping the whole string in <i> gets it wrong - and that is what most sites do. Ours too: we noticed it on PlantGeekz, our plant app, while building the library this post is about.
The input is worse than the output
Getting the italics right is the easy half. The hard half is that plant names arrive from spreadsheets, nursery catalogues and user input in every shape imaginable:
hydrangea x macrophylla ssp. serrata cv. Bluebird
HYDRANGEA ×MACROPHYLLA
Hosta ‘Blue Angel’ ← curly quotes from iOS
Mentha x piperita L.
Brassica oleracea (Capitata Group) 'Brunswick'
Rosa Flower Carpet® 'Noare'
Salix alba × S. fragilis ← a hybrid formula, not a species
x should be ×. ssp. should be subsp.. cv. Bluebird should be 'Bluebird'. And you need to know which part is which before you can italicize anything.
There are great scientific name parsers - gnparser in Go and GBIF's name-parser in Java - but nothing small that runs in a PHP web app or directly in the browser. So we built one.
botanical-name
It's a zero-dependency library, published for PHP and JavaScript/TypeScript. Both packages give byte-identical results.
import { parse, format, toHtml, key } from '@plantgeekz_com/botanical-name';
const name = parse('hydrangea x macrophylla ssp. serrata cv. Bluebird');
format(name);
// Hydrangea ×macrophylla subsp. serrata 'Bluebird'
toHtml(name);
// <i>Hydrangea</i> ×<i>macrophylla</i> subsp. <i>serrata</i> 'Bluebird'
key(name);
// hydrangea macrophylla subsp. serrata bluebird
The same in PHP:
use PlantGeekz\BotanicalName\BotanicalName;
$name = BotanicalName::parse('hydrangea x macrophylla ssp. serrata cv. Bluebird');
$name->toString(); // Hydrangea ×macrophylla subsp. serrata 'Bluebird'
$name->toHtml(); // <i>Hydrangea</i> ×<i>macrophylla</i> subsp. <i>serrata</i> 'Bluebird'
parse() gives you the parts - genus, epithet, authors, infraspecific ranks, group, trade designation, cultivar - and a list of warnings for everything it had to fix, so you can flag messy data instead of silently changing it:
parse('Rosa canina ssp. canina').warnings;
// ['rank_normalized']
key() strips authors, quotes, hybrid signs and diacritics, which makes it handy for matching and de-duplicating names from different sources.
What 30,569 real names taught us
We wrote the rules first and a set of hand-made test cases second. Everything passed. Then we ran both implementations over 30,569 real names from the PlantGeekz taxonomy - species with authors and thousands of cultivars - and checked that every clean name came back unchanged.
Three things broke that our own test cases had never covered.
1. Apostrophes inside cultivar names. To find a cultivar, you look for text in single quotes. A lazy regex stops at the first closing quote, and that works for 'Blue Angel'. It falls apart on the names daylily and dahlia breeders actually register:
Dahlia 'Rees' Choice'
Hemerocallis 'Burnin' Down the House'
Dahlia 'Hugs 'N' Kisses'
Hemerocallis 'Tam O' Shanter'
The fix was to match greedily - from the first opening quote to the last closing one - which, it turned out, our own backend was already doing.
2. A capital X inside a name. Hemerocallis 'Longfields X Factor' was read as a hybrid formula - Hemerocallis 'Longfields crossed with Factor' - because X between two capitalised words is how formulas are written. The formula detector now tracks whether it is inside quotes.
3. A name in brackets. Seed catalogues write cultivar groups as Brassica oleracea (Capitata), so we treated a trailing name in brackets as a group. In the real data, Cyathea minervae (Lehnert) and Justicia carrissoi (Benoist) turned out to be incomplete author citations, not groups. Now a bare bracket only counts as a group when a cultivar follows it.
None of these showed up in the tests we wrote ourselves. All three showed up within minutes on real data.
Keeping PHP and TypeScript identical
Two implementations of the same parser will drift apart, so we set up three guards:
-
Shared fixtures. The PHP repo owns
fixtures/names.json- currently 60+ cases with the expected parts, text, HTML, slug and key. The JS repo keeps a copy, and CI fails if the copy differs from the PHP one. -
A line-by-line port. The TypeScript parser mirrors the PHP one method for method, including the parts that differ between the languages. JavaScript's
\sandtrim()match more Unicode whitespace than PHP's do, for instance, so both use the same explicit character class. - Differential testing. Before release, the 30,569 names went through both implementations and the full output was compared byte for byte. Zero differences, including after each of the three fixes above.
Scope, honestly
It's a structural parser. It reads the shape of a name and never looks it up, so it can tell you what the parts are but not whether the name exists or is accepted. For that you need a taxonomic backbone such as GBIF. It also doesn't handle zoological or bacterial names.
Try it
composer require plantgeekz/botanical-name
npm install @plantgeekz_com/botanical-name
deno add jsr:@plantgeekz/botanical-name
The code is MIT-licensed on GitHub (PHP, JS/TS). If you have a plant name it gets wrong, open an issue with the name, and it will go into the fixtures.
We built this for PlantGeekz, our plant identification and collection app. If you grow plants yourself, have a look.
Top comments (0)