Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the ability to normalize wrapped objects #138

Merged
merged 13 commits into from
Jun 18, 2024
1 change: 1 addition & 0 deletions .node-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
20.12.2
428 changes: 232 additions & 196 deletions package-lock.json

Large diffs are not rendered by default.

18 changes: 18 additions & 0 deletions src/wrappers/CitationWrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,24 @@ class CitationWrapper {
this.citation = citation;
}

/**
* Return a normalized form of a citation.
*
* I'm not really sure how to normalize a citation, but the main thing we can do is delete any key
* that is equivalent to ''. We could interconvert between `name` and
* `firstname/lastname/middlename`, but that's not really equivalent, is it?
*/
static normalize(citation) {
const normalizedCitation = {};
Object.keys(citation).forEach((key) => {
// As long as citation[key] has a reasonable value, we copy it into the normalized citation.
if (citation[key]) {
normalizedCitation[key] = citation[key];
}
});
return normalizedCitation;
}

/**
* Helper method to return a single name for a given agent entry.
* The algorithm we use is:
Expand Down
21 changes: 20 additions & 1 deletion src/wrappers/PhylogenyWrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
* PhylogenyWrapper
*/

const { has } = require('lodash');
const {
has,
cloneDeep,
} = require('lodash');

/** Used to parse Newick strings. */
const newickJs = require('newick-js');
Expand Down Expand Up @@ -34,6 +37,22 @@ class PhylogenyWrapper {
this.defaultNomenCode = defaultNomenCode;
}

/**
* Return a normalized form of the phylogeny.
*/
static normalize(phylogeny) {
const normalizedPhylogeny = cloneDeep(phylogeny);

// We could normalize the Newick string, but that doesn't seem very nice.

// Normalize the source if there is one.
if ('source' in phylogeny) {
normalizedPhylogeny.source = CitationWrapper.normalize(phylogeny.source || {});
}

return normalizedPhylogeny;
}

static getErrorsInNewickString(newick) {
// Given a Newick string, return a list of errors found in parsing this
// string. The errors are returned as a list of objects, each of which
Expand Down
16 changes: 16 additions & 0 deletions src/wrappers/PhylorefWrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,22 @@ class PhylorefWrapper {
return this.phyloref.internalSpecifiers;
}

/**
* Normalize a phyloreference.
*
* @param phyloref
*/
static normalize(phyloref) {
const normalizedPhyloref = cloneDeep(phyloref);

normalizedPhyloref.internalSpecifiers = (phyloref.internalSpecifiers || [])
.map(TaxonomicUnitWrapper.normalize);
normalizedPhyloref.externalSpecifiers = (phyloref.externalSpecifiers || [])
.map(TaxonomicUnitWrapper.normalize);

return normalizedPhyloref;
}

/** Return the external specifiers of this phyloref (if any). */
get externalSpecifiers() {
if (!has(this.phyloref, 'externalSpecifiers')) {
Expand Down
26 changes: 26 additions & 0 deletions src/wrappers/PhyxWrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,32 @@ class PhyxWrapper {
return owlterms.UNKNOWN_CODE;
}

/**
* Return a provided Phyx document as a normalized JSON document. We ignore most keys -- including
* keys we don't know -- but any key that can be wrapped by one of the other Wrappers in this
* package will be wrapped and normalized before being returned.
*
* Normalization is mostly needed for TaxonomicUnitWrappers and its subclasses
* (TaxonConceptWrapper, TaxonNameWrapper), since these can be represented in several essentially
* identical ways. But if we implement it at every level, we can implement comparison code in
* Klados easily.
*
* Two Phyx documents should -- upon being normalized -- be comparable with each other with
* lodash.deepEqual().
*/
static normalize(phyxDocument) {
const normalizedDocument = cloneDeep(phyxDocument);

normalizedDocument.phylorefs = (phyxDocument.phylorefs || []).map(PhylorefWrapper.normalize);
normalizedDocument.phylogenies = (phyxDocument.phylogenies || [])
.map(PhylogenyWrapper.normalize);
if ('source' in phyxDocument) {
normalizedDocument.source = CitationWrapper.normalize(phyxDocument.source);
}

return normalizedDocument;
}

/**
* Generate an executable ontology from this Phyx document. The document is mostly in JSON-LD
* already, except for three important things:
Expand Down
19 changes: 19 additions & 0 deletions src/wrappers/SpecimenWrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,25 @@ class SpecimenWrapper {
this.specimen = specimen;
}

/**
* Normalize the specified specimen.
* @param specimen A specimen to be normalized.
*/
static normalize(specimen) {
const wrapped = new SpecimenWrapper(specimen);
const normalizedSpecimen = {
'@type': SpecimenWrapper.TYPE_SPECIMEN,
label: wrapped.label,
'dwc:basisOfRecord': wrapped.basisOfRecord,
occurrenceID: wrapped.occurrenceID,
catalogNumber: wrapped.catalogNumber,
institutionCode: wrapped.institutionCode,
collectionCode: wrapped.collectionCode,
};
if ('@id' in specimen) normalizedSpecimen['@id'] = specimen['@id'];
return normalizedSpecimen;
}

/**
* Parse the provided occurrence ID. The two expected formats are:
* - 'urn:catalog:[institutionCode]:[collectionCode]:[catalogNumber]'
Expand Down
25 changes: 21 additions & 4 deletions src/wrappers/TaxonConceptWrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,23 @@ class TaxonConceptWrapper {
this.defaultNomenCode = defaultNomenCode;
}

/**
* Normalize the specified taxon concept.
* @param tc A taxon concept to be normalized.
*/
static normalize(tc) {
const wrapped = new TaxonConceptWrapper(tc);
const normalizedTC = {
'@type': TaxonConceptWrapper.TYPE_TAXON_CONCEPT,
label: wrapped.label,
hasName: TaxonNameWrapper.normalize(wrapped.taxonName),
nameString: wrapped.taxonName.nameComplete,
accordingTo: wrapped.accordingTo,
};
if ('@id' in tc) normalizedTC['@id'] = tc['@id'];
return normalizedTC;
}

/**
* Return the taxon name of this taxon concept (if any) as an object.
*/
Expand Down Expand Up @@ -89,10 +106,10 @@ class TaxonConceptWrapper {
*/
get accordingTo() {
// Do we have any accordingTo information?
if (has(this.tunit, 'accordingTo')) return this.type.accordingTo;
if (has(this.tunit, 'accordingTo')) return this.tunit.accordingTo;

// Do we have an accordingToString?
if (has(this.tunit, 'accordingToString')) return this.type.accordingToString;
if (has(this.tunit, 'accordingToString')) return this.tunit.accordingToString;

// If not, we have no accodingTo information!
return undefined;
Expand All @@ -106,10 +123,10 @@ class TaxonConceptWrapper {
*/
get accordingToString() {
// Do we have any accordingTo information?
if (has(this.tunit, 'accordingTo')) return JSON.stringify(this.type.accordingTo);
if (has(this.tunit, 'accordingTo')) return JSON.stringify(this.tunit.accordingTo);

// Do we have an accordingToString?
if (has(this.tunit, 'accordingToString')) return this.type.accordingToString;
if (has(this.tunit, 'accordingToString')) return this.tunit.accordingToString;

// If not, we have no accodingTo information!
return undefined;
Expand Down
19 changes: 19 additions & 0 deletions src/wrappers/TaxonNameWrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,25 @@ class TaxonNameWrapper {
return undefined;
}

/**
* Normalize the specified taxon name.
* @param txname A taxon name to be normalized.
*/
static normalize(txname) {
const wrapped = new TaxonNameWrapper(txname);
const normalizedTxname = {
'@type': TaxonNameWrapper.TYPE_TAXON_NAME,
nomenclaturalCode: wrapped.nomenclaturalCode,
label: wrapped.label,
nameComplete: wrapped.nameComplete,
genusPart: wrapped.genusPart,
specificEpithet: wrapped.specificEpithet,
infraspecificEpithet: wrapped.infraspecificEpithet,
};
if ('@id' in txname) normalizedTxname['@id'] = txname['@id'];
return normalizedTxname;
}

/**
* Returns the nomenclatural code of this taxon name.
*/
Expand Down
19 changes: 19 additions & 0 deletions src/wrappers/TaxonomicUnitWrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,25 @@ class TaxonomicUnitWrapper {
this.defaultNomenCode = defaultNomenCode;
}

/**
* Normalize the specified taxonomic unit.
* @param tunit A taxonomic unit to be normalized.
*/
static normalize(tunit) {
const wrapped = new TaxonomicUnitWrapper(tunit);
if (wrapped.taxonConcept) {
return TaxonConceptWrapper.normalize(tunit);
}
if (wrapped.specimen) {
return SpecimenWrapper.normalize(tunit);
}
if (wrapped.externalReferences) {
// External references should only have an `@id`.
return tunit;
}
return tunit;
}

/**
* What type of specifier is this? This is an array that could contain multiple
* classes, but should contain one of:
Expand Down
Loading
Loading