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 "lexical" and "natural" sort strategies for notation codes #1205

Merged
merged 8 commits into from
Sep 16, 2021
2 changes: 1 addition & 1 deletion model/Concept.php
Original file line number Diff line number Diff line change
Expand Up @@ -558,7 +558,7 @@ public function getProperties()
if ($superprop) {
$superprop = EasyRdf\RdfNamespace::shorten($superprop) ? EasyRdf\RdfNamespace::shorten($superprop) : $superprop;
}
$sort_by_notation = $this->vocab->getConfig()->sortByNotation();
$sort_by_notation = $this->vocab->getConfig()->getSortByNotation();
$propobj = new ConceptProperty($prop, $proplabel, $prophelp, $superprop, $sort_by_notation);

if ($propobj->getLabel() !== null) {
Expand Down
8 changes: 6 additions & 2 deletions model/ConceptProperty.php
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,12 @@ private function sortValues()
return -1;
}
else {
// assume that notations are unique
return strnatcasecmp($anot, $bnot);
// assume that notations are unique, choose strategy
if ($this->sort_by_notation == "lexical") {
return strcoll($anot, $bnot);
} else { // natural
return strnatcasecmp($anot, $bnot);
}
}
});
}
Expand Down
7 changes: 1 addition & 6 deletions model/ConceptPropertyValue.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,7 @@ public function __construct($model, $vocab, $resource, $prop, $clang = '')

public function __toString()
{
$label = is_string($this->getLabel()) ? $this->getLabel() : $this->getLabel()->getValue();
if ($this->vocab->getConfig()->sortByNotation()) {
$label = ltrim($this->getNotation() . ' ') . $label;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding the notation here doesn't seem to have any effect on the sort order so I just removed it.

}

return $label;
return is_string($this->getLabel()) ? $this->getLabel() : $this->getLabel()->getValue();
}

public function getLang()
Expand Down
17 changes: 13 additions & 4 deletions model/VocabularyConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -178,12 +178,21 @@ public function getTitle($lang = null)
}

/**
* Returns a boolean value set in the config.ttl config.
* @return boolean
* Returns the sorting strategy for notation codes set in the config.ttl
* config: either "lexical" (default), "natural", or null if
* sorting by notations is disabled.
* @return string|bool
*/
public function sortByNotation()
public function getSortByNotation(): ?string
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I renamed the method for better consistency with other getters, and redefined its return value.

{
return $this->getBoolean('skosmos:sortByNotation');
$value = $this->getLiteral('skosmos:sortByNotation');
if ($value == "lexical" || $value == "natural") {
return $value;
}
// not a special value - interpret as boolean instead
$bvalue = $this->getBoolean('skosmos:sortByNotation');
// default sorting strategy is "lexical"
return $bvalue ? "lexical" : null;
}

/**
Expand Down
43 changes: 29 additions & 14 deletions resource/js/hierarchy.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,9 @@ function getLabel(object) {
labelProp = 'label';
}
if (window.showNotation && object.notation) {
return '<span class="tree-notation">' + object.notation + '</span> ' + object[labelProp];
return '<span class="tree-notation">' + object.notation + '</span> <span class="tree-label">' + escapeHtml(object[labelProp]) + '</span>';
}
return escapeHtml(object[labelProp]);
return '<span class="tree-label">' + escapeHtml(object[labelProp]) + '</span>';
}

function createObjectsFromChildren(conceptData, conceptUri) {
Expand Down Expand Up @@ -423,6 +423,22 @@ function topConceptsToSchemes(topConcepts, schemes) {
return childArray;
}

/*
* Return a sort key suitable for sorting hierarchy nodes mainly by label.
* Nodes with domain class will be sorted first, followed by non-domain nodes.
*/
function nodeLabelSortKey(node) {
// make sure the tree nodes with class 'domain' are sorted before the others
// domain will be "0" if the node has a domain class, else "1"
var domain = (node.original.a_attr['class'] == 'domain') ? "0" : "1";

// parse the HTML code in node.text and return just the label as a lower case value for sorting
// should look like '<span class="tree-notation">12.3</span> <span class="tree-label">Hello</span>'
label = $(node.text.toLowerCase()).filter('.tree-label').text();

return domain + " " + label;
}

/*
* Gives you the Skosmos default jsTree configuration.
*/
Expand Down Expand Up @@ -508,17 +524,21 @@ function getTreeConfiguration() {
var bNode = this.get_node(b);

// sort on notation if requested, and notations exist
if (window.showNotation) {
if (window.sortByNotation) {
var aNotation = aNode.original.notation;
var bNotation = bNode.original.notation;

if (aNotation) {
if (bNotation) {
if (aNotation < bNotation) {
return -1;
}
else if (aNotation > bNotation) {
return 1;
if (window.sortByNotation == "lexical") {
if (aNotation < bNotation) {
return -1;
}
else if (aNotation > bNotation) {
return 1;
}
} else { // natural
return naturalCompare(aNotation, bNotation);
}
}
else return -1;
Expand All @@ -527,12 +547,7 @@ function getTreeConfiguration() {
// NOTE: if no notations found, fall back on label comparison below
}
// no sorting on notation requested, or notations don't exist
// make sure the tree nodes with class 'domain' are sorted before the others
// aDomain/bDomain will be "0" if a/b has a domain class, else "1"
var aDomain = (aNode.original.a_attr['class'] == 'domain') ? "0" : "1";
var bDomain = (bNode.original.a_attr['class'] == 'domain') ? "0" : "1";
return naturalCompare(aDomain + " " + aNode.text.toLowerCase(),
bDomain + " " + bNode.text.toLowerCase());
return naturalCompare(nodeLabelSortKey(aNode), nodeLabelSortKey(bNode));
}
});
}
Expand Down
11 changes: 9 additions & 2 deletions resource/js/scripts.js
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,14 @@ function countAndSetOffset() {
}
}

// return -1 if the value is negative, 1 otherwise
// used to coerce sort values so they are compatible with the jsTree sort plugin
function negVsPos(val) {
return (val < 0) ? -1 : 1;
}

// Natural sort from: http://stackoverflow.com/a/15479354/3894569
// adapted to return only -1 or 1 using negVsPos function above
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at the documentation of the jsTree sort plugin, it expects that the return value of the sort function is either -1 or 1 (anything other than -1 will be interpreted as 1, AFAICT). To make sure that the return values meet those expectations, I added a coercing function.

function naturalCompare(a, b) {
var ax = [], bx = [];

Expand All @@ -337,10 +344,10 @@ function naturalCompare(a, b) {
var an = ax.shift();
var bn = bx.shift();
var nn = (an[0] - bn[0]) || an[1].localeCompare(bn[1], lang);
if(nn) return nn;
if(nn) return negVsPos(nn);
}

return ax.length - bx.length;
return negVsPos(ax.length - bx.length);
}

function makeCallbacks(data, pageType) {
Expand Down
44 changes: 42 additions & 2 deletions tests/ConceptPropertyTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -123,12 +123,52 @@ public function testAddValue() {
* @covers ConceptProperty::addValue
* @covers ConceptProperty::sortValues
*/
public function testSortNotatedValues() {
public function testSortNotatedValuesLexical() {
# the vocabulary is configured to use lexical sorting
$vocab = $this->model->getVocabulary('test-notation-sort');
$concepts = $vocab->getConceptInfo('http://www.skosmos.skos/test/ta01', 'en');
$concept = $concepts[0];
$props = $concept->getProperties();
$expected = array("test:ta0112", "test:ta0119", "test:ta0117", "test:ta0116", "test:ta0114","test:ta0115","test:ta0113", "test:ta0120", "test:ta0111", );
$expected = array(
"test:ta0111", # 33.01
"test:ta0116", # 33.02
"test:ta0112", # 33.1
"test:ta0114", # 33.10
"test:ta0115", # 33.2
"test:ta0117", # 33.9
"test:ta0119", # 33.90
"test:ta0120", # K2
"test:ta0113" # concept not defined, no notation code
);
$ret = array();

foreach($props['skos:narrower']->getValues() as $val) {
$ret[] = EasyRdf\RdfNamespace::shorten($val->getUri());
}
$this->assertEquals($expected, $ret);
}

/**
* @covers ConceptProperty::addValue
* @covers ConceptProperty::sortValues
*/
public function testSortNotatedValuesNatural() {
# the vocabulary is configured to use natural sorting
$vocab = $this->model->getVocabulary('testNotation');
$concepts = $vocab->getConceptInfo('http://www.skosmos.skos/test/ta01', 'en');
$concept = $concepts[0];
$props = $concept->getProperties();
$expected = array(
"test:ta0111", # 33.01
"test:ta0116", # 33.02
"test:ta0112", # 33.1
"test:ta0115", # 33.2
"test:ta0117", # 33.9
"test:ta0114", # 33.10
"test:ta0119", # 33.90
"test:ta0120", # K2
"test:ta0113" # concept not defined, no notation code
);
$ret = array();

foreach($props['skos:narrower']->getValues() as $val) {
Expand Down
38 changes: 0 additions & 38 deletions tests/ConceptPropertyValueTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -130,44 +130,6 @@ public function testToStringWhenSortByNotationNotSet() {
$this->assertEquals('Carp', (string)$propvals['665 Carp http://www.skosmos.skos/test/ta112']);
}

/**
* @covers ConceptPropertyValue::__toString
*/
public function testToStringWithNotation() {
$mockres = $this->getMockBuilder('EasyRdf\\Resource')->disableOriginalConstructor()->getMock();
$mockvoc = $this->getMockBuilder('Vocabulary')->disableOriginalConstructor()->getMock();
$mockconf = $this->getMockBuilder('VocabularyConfig')->disableOriginalConstructor()->getMock();
$mocklit = $this->getMockBuilder('EasyRdf\\Literal')->disableOriginalConstructor()->getMock();
$mocklit->method('getValue')->will($this->returnValue('T3ST'));
$mockconf->method('sortByNotation')->will($this->returnValue(true));
$mockconf->method('showNotation')->will($this->returnValue(true));
$mockvoc->method('getConfig')->will($this->returnValue($mockconf));
$mockres->method('label')->will($this->returnValue('Term label'));
$mockres->method('get')->will($this->returnValue($mocklit));
$mockres->method('getUri')->will($this->returnValue('http://thisdoesntexistatalland.sefsf/2j2h4/'));
$propval = new ConceptPropertyValue($this->model, $mockvoc, $mockres, null);
$this->assertEquals('T3ST Term label', (string)$propval);
}

/**
* @covers ConceptPropertyValue::__toString
*/
public function testToStringWhenNotationExistsButIsConfiguredOff() {
$mockres = $this->getMockBuilder('EasyRdf\\Resource')->disableOriginalConstructor()->getMock();
$mockvoc = $this->getMockBuilder('Vocabulary')->disableOriginalConstructor()->getMock();
$mockconf = $this->getMockBuilder('VocabularyConfig')->disableOriginalConstructor()->getMock();
$mocklit = $this->getMockBuilder('EasyRdf\\Literal')->disableOriginalConstructor()->getMock();
$mocklit->method('getValue')->will($this->returnValue('T3ST'));
$mockconf->method('sortByNotation')->will($this->returnValue(true));
$mockconf->method('showNotation')->will($this->returnValue(false));
$mockvoc->method('getConfig')->will($this->returnValue($mockconf));
$mockres->method('label')->will($this->returnValue('Term label'));
$mockres->method('get')->will($this->returnValue($mocklit));
$mockres->method('getUri')->will($this->returnValue('http://thisdoesntexistatalland.sefsf/2j2h4/'));
$propval = new ConceptPropertyValue($this->model, $mockvoc, $mockres, null);
$this->assertEquals('Term label', (string)$propval);
}

/**
* @covers ConceptPropertyValue::addSubMember
* @covers ConceptPropertyValue::sortSubMembers
Expand Down
35 changes: 33 additions & 2 deletions tests/VocabularyConfigTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -318,12 +318,43 @@ public function testShowChangeListDefaultValue() {
}

/**
* @covers VocabularyConfig::sortByNotation
* @covers VocabularyConfig::getSortByNotation
* @covers VocabularyConfig::getLiteral
* @covers VocabularyConfig::getBoolean
*/
public function testShowSortByNotationDefaultValue() {
$vocab = $this->model->getVocabulary('test');
$this->assertEquals(false, $vocab->getConfig()->sortByNotation());
$this->assertNull($vocab->getConfig()->getSortByNotation());
}

/**
* @covers VocabularyConfig::getSortByNotation
* @covers VocabularyConfig::getLiteral
* @covers VocabularyConfig::getBoolean
*/
public function testShowSortByNotationTrueIsLexical() {
$vocab = $this->model->getVocabulary('test-notation-sort');
$this->assertEquals("lexical", $vocab->getConfig()->getSortByNotation());
}

/**
* @covers VocabularyConfig::getSortByNotation
* @covers VocabularyConfig::getLiteral
* @covers VocabularyConfig::getBoolean
*/
public function testShowSortByNotationLexical() {
$vocab = $this->model->getVocabulary('test-qualified-notation');
$this->assertEquals("lexical", $vocab->getConfig()->getSortByNotation());
}

/**
* @covers VocabularyConfig::getSortByNotation
* @covers VocabularyConfig::getLiteral
* @covers VocabularyConfig::getBoolean
*/
public function testShowSortByNotationNatural() {
$vocab = $this->model->getVocabulary('testNotation');
$this->assertEquals("natural", $vocab->getConfig()->getSortByNotation());
}

/**
Expand Down
10 changes: 8 additions & 2 deletions tests/test-vocab-data/test-notation-sort.ttl
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,12 @@ test:ta0111 a skos:Concept, meta:TestClass ;
skos:broader test:ta01 ;
skos:inScheme test:conceptscheme ;
owl:deprecated true ;
skos:notation "33.01" ;
skos:prefLabel "Tuna"@en .

test:ta0112 a skos:Concept, meta:TestClass ;
skosmos:testprop "Test property value" ;
skos:notation "665" ;
skos:notation "33.1" ;
skos:broader test:ta01 ;
skos:narrower test:ta0121 ;
skos:exactMatch test:ta0118 ;
Expand All @@ -56,6 +57,7 @@ test:ta0112 a skos:Concept, meta:TestClass ;
test:ta0114 a skos:Concept, meta:TestClass ;
skos:broader test:ta01 ;
dct:modified "1986-21-00"^^xsd:date ; # date invalid on purpose
skos:notation "33.10" ;
skos:inScheme test:conceptscheme ;
skos:prefLabel "Buri"@en .

Expand All @@ -64,16 +66,19 @@ test:ta0115 a skos:Concept, meta:TestClass ;
skos:inScheme test:conceptscheme ;
skos:definition "any fish belonging to the order Anguilliformes"@en,
"Iljettävä limanuljaska"@fi ;
skos:notation "33.2" ;
skos:prefLabel "Eel"@en .

test:ta0116 a skos:Concept, meta:TestClass ;
skos:broader test:ta01 ;
skos:inScheme test:conceptscheme ;
skos:notation "33.02" ;
skos:prefLabel "Barracuda"@en .

test:ta0117 a skos:Concept, meta:TestClass ;
skos:broader test:ta01 ;
skos:inScheme test:conceptscheme ;
skos:notation "33.9" ;
skos:relatedMatch test:ta0115 ;
skos:prefLabel "3D Barracuda"@en .

Expand All @@ -85,11 +90,12 @@ test:ta0118 a skos:Concept, meta:TestClass ;
test:ta0119 a skos:Concept, meta:TestClass ;
skos:broader test:ta01 ;
skos:inScheme test:conceptscheme ;
skos:notation "690" ;
skos:notation "33.90" ;
skos:prefLabel "Hauki"@fi .

test:ta0120 a skos:Concept, meta:TestClass ;
skos:broader test:ta01 ;
skos:notation "K2" ;
skos:inScheme test:conceptscheme .

test:ta0121 a skos:Concept, meta:TestClass ;
Expand Down
7 changes: 4 additions & 3 deletions tests/testconfig.ttl
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@
void:uriSpace "http://www.skosmos.skos/test-qualified-notation/" ;
skosmos:defaultLanguage "en" ;
skosmos:groupClass skos:Collection ;
skosmos:sortByNotation "lexical" ;
skosmos:language "en" ;
skosmos:alphabeticalListQualifier skos:notation ;
skosmos:sparqlGraph <http://www.skosmos.skos/test-qualified-notation/> .
Expand Down Expand Up @@ -335,13 +336,13 @@
:testNotation a skosmos:Vocabulary, void:Dataset ;
dc11:title "A vocabulary for testing vocabularies with notation features"@en ;
dc:subject :cat_general ;
void:uriSpace "http://www.skosmos.skos/notationFeatures/";
void:uriSpace "http://www.skosmos.skos/test-notation-sort/";
skosmos:language "fi";
skosmos:sortByNotation true ;
skosmos:sortByNotation "natural" ;
skosmos:searchByNotation true ;
skosmos:showNotation true ;
skosmos:useModifiedDate "true";
skosmos:sparqlGraph <http://www.skosmos.skos/notation/>;
skosmos:sparqlGraph <http://www.skosmos.skos/test-notation-sort/>;
skosmos:mainConceptScheme test:notationMainConceptScheme .

:paramPluginTest a skosmos:Vocabulary, void:Dataset ;
Expand Down
Loading