Skip to content

Commit

Permalink
fix: ROUND, ENCODE_FOR_URI and SECONDS SPARQL functions (#2314)
Browse files Browse the repository at this point in the history
`ROUND` was not correctly rounding negative numbers towards positive infinity,
`ENCODE_FOR_URI` incorrectly treated `/` as safe, and `SECONDS` did not include
fractional seconds.

This change corrects these issues.

- Closes <#2151>.
  • Loading branch information
aucampia committed Mar 26, 2023
1 parent 57bb428 commit af17916
Show file tree
Hide file tree
Showing 2 changed files with 196 additions and 4 deletions.
11 changes: 7 additions & 4 deletions rdflib/plugins/sparql/operators.py
Expand Up @@ -16,7 +16,7 @@
import re
import uuid
import warnings
from decimal import ROUND_HALF_UP, Decimal, InvalidOperation
from decimal import ROUND_HALF_DOWN, ROUND_HALF_UP, Decimal, InvalidOperation
from functools import reduce
from typing import Any, Callable, Dict, NoReturn, Optional, Tuple, Union, overload
from urllib.parse import quote
Expand Down Expand Up @@ -205,7 +205,7 @@ def Builtin_ROUND(expr: Expr, ctx) -> Literal:
# this is an ugly work-around
l_ = expr.arg
v = numeric(l_)
v = int(Decimal(v).quantize(1, ROUND_HALF_UP))
v = int(Decimal(v).quantize(1, ROUND_HALF_UP if v > 0 else ROUND_HALF_DOWN))
return Literal(v, datatype=l_.datatype)


Expand Down Expand Up @@ -381,7 +381,7 @@ def Builtin_CONTAINS(expr: Expr, ctx) -> Literal:


def Builtin_ENCODE_FOR_URI(expr: Expr, ctx) -> Literal:
return Literal(quote(string(expr.arg).encode("utf-8")))
return Literal(quote(string(expr.arg).encode("utf-8"), safe=""))


def Builtin_SUBSTR(expr: Expr, ctx) -> Literal:
Expand Down Expand Up @@ -471,7 +471,10 @@ def Builtin_SECONDS(e: Expr, ctx) -> Literal:
http://www.w3.org/TR/sparql11-query/#func-seconds
"""
d = datetime(e.arg)
return Literal(d.second, datatype=XSD.decimal)
result_value = Decimal(d.second)
if d.microsecond:
result_value += Decimal(d.microsecond) / Decimal(1000000)
return Literal(result_value, datatype=XSD.decimal)


def Builtin_TIMEZONE(e: Expr, ctx) -> Literal:
Expand Down
189 changes: 189 additions & 0 deletions test/test_sparql/test_functions.py
@@ -0,0 +1,189 @@
import logging
from decimal import Decimal

import pytest

from rdflib.graph import Graph
from rdflib.namespace import XSD, Namespace
from rdflib.plugins.sparql.operators import _lang_range_check
from rdflib.term import BNode, Identifier, Literal, URIRef

EG = Namespace("https://example.com/")


@pytest.mark.parametrize(
["expression", "expected_result"],
[
(r"isIRI('eg:IRI')", Literal(False)),
(r"isIRI(eg:IRI)", Literal(True)),
(r"isURI('eg:IRI')", Literal(False)),
(r"isURI(eg:IRI)", Literal(True)),
(r"isBLANK(eg:IRI)", Literal(False)),
(r"isBLANK(BNODE())", Literal(True)),
(r"isLITERAL(eg:IRI)", Literal(False)),
(r"isLITERAL('eg:IRI')", Literal(True)),
(r"isNumeric(eg:IRI)", Literal(False)),
(r"isNumeric(1)", Literal(True)),
(r"STR(eg:IRI)", Literal("https://example.com/IRI")),
(r"STR(1)", Literal("1")),
(r'LANG("Robert"@en)', Literal("en")),
(r'LANG("Robert")', Literal("")),
(r'DATATYPE("Robert")', XSD.string),
(r'DATATYPE("42"^^xsd:integer)', XSD.integer),
(r'IRI("http://example/")', URIRef("http://example/")),
(r'BNODE("example")', BNode),
(r'STRDT("123", xsd:integer)', Literal("123", datatype=XSD.integer)),
(r'STRLANG("cats and dogs", "en")', Literal("cats and dogs", lang="en")),
(r"UUID()", URIRef),
(r"STRUUID()", Literal),
(r'STRLEN("chat")', Literal(4)),
(r'SUBSTR("foobar", 4)', Literal("bar")),
(r'UCASE("foo")', Literal("FOO")),
(r'LCASE("BAR")', Literal("bar")),
(r'strStarts("foobar", "foo")', Literal(True)),
(r'strStarts("foobar", "bar")', Literal(False)),
(r'strEnds("foobar", "bar")', Literal(True)),
(r'strEnds("foobar", "foo")', Literal(False)),
(r'contains("foobar", "bar")', Literal(True)),
(r'contains("foobar", "barfoo")', Literal(False)),
(r'strbefore("abc","b")', Literal("a")),
(r'strbefore("abc","xyz")', Literal("")),
(r'strafter("abc","b")', Literal("c")),
(r'strafter("abc","xyz")', Literal("")),
(r"ENCODE_FOR_URI('this/is/a/test')", Literal("this%2Fis%2Fa%2Ftest")),
(r"ENCODE_FOR_URI('this is a test')", Literal("this%20is%20a%20test")),
(
r"ENCODE_FOR_URI('AAA~~0123456789~~---~~___~~...~~ZZZ')",
Literal("AAA~~0123456789~~---~~___~~...~~ZZZ"),
),
(r'CONCAT("foo", "bar")', Literal("foobar")),
(r'langMatches(lang("That Seventies Show"@en), "en")', Literal(True)),
(
r'langMatches(lang("Cette Série des Années Soixante-dix"@fr), "en")',
Literal(False),
),
(
r'langMatches(lang("Cette Série des Années Septante"@fr-BE), "en")',
Literal(False),
),
(r'langMatches(lang("Il Buono, il Bruto, il Cattivo"), "en")', Literal(False)),
(r'langMatches(lang("That Seventies Show"@en), "FR")', Literal(False)),
(
r'langMatches(lang("Cette Série des Années Soixante-dix"@fr), "FR")',
Literal(True),
),
(
r'langMatches(lang("Cette Série des Années Septante"@fr-BE), "FR")',
Literal(True),
),
(r'langMatches(lang("Il Buono, il Bruto, il Cattivo"), "FR")', Literal(False)),
(r'langMatches(lang("That Seventies Show"@en), "*")', Literal(True)),
(
r'langMatches(lang("Cette Série des Années Soixante-dix"@fr), "*")',
Literal(True),
),
(
r'langMatches(lang("Cette Série des Années Septante"@fr-BE), "*")',
Literal(True),
),
(r'langMatches(lang("Il Buono, il Bruto, il Cattivo"), "*")', Literal(False)),
(r'langMatches(lang("abc"@en-gb), "en-GB")', Literal(True)),
(r'regex("Alice", "^ali", "i")', Literal(True)),
(r'regex("Bob", "^ali", "i")', Literal(False)),
(r'replace("abcd", "b", "Z")', Literal("aZcd")),
(r"abs(-1.5)", Literal("1.5", datatype=XSD.decimal)),
(r"round(2.4999)", Literal("2", datatype=XSD.decimal)),
(r"round(2.5)", Literal("3", datatype=XSD.decimal)),
(r"round(-2.5)", Literal("-2", datatype=XSD.decimal)),
(r"round(0.1)", Literal("0", datatype=XSD.decimal)),
(r"round(-0.1)", Literal("0", datatype=XSD.decimal)),
(r"RAND()", Literal),
(r"now()", Literal),
(r'month("2011-01-10T14:45:13.815-05:00"^^xsd:dateTime)', Literal(1)),
(r'day("2011-01-10T14:45:13.815-05:00"^^xsd:dateTime)', Literal(10)),
(r'hours("2011-01-10T14:45:13.815-05:00"^^xsd:dateTime)', Literal(14)),
(r'minutes("2011-01-10T14:45:13.815-05:00"^^xsd:dateTime)', Literal(45)),
(
r'seconds("2011-01-10T14:45:13.815-05:00"^^xsd:dateTime)',
Literal(Decimal("13.815")),
),
(
r'timezone("2011-01-10T14:45:13.815-05:00"^^xsd:dateTime)',
Literal("-PT5H", datatype=XSD.dayTimeDuration),
),
(
r'timezone("2011-01-10T14:45:13.815Z"^^xsd:dateTime)',
Literal("PT0S", datatype=XSD.dayTimeDuration),
),
(
r'tz("2011-01-10T14:45:13.815-05:00"^^xsd:dateTime) ',
Literal("-05:00"),
),
(
r'tz("2011-01-10T14:45:13.815Z"^^xsd:dateTime) ',
Literal("Z"),
),
(
r'tz("2011-01-10T14:45:13.815"^^xsd:dateTime) ',
Literal(""),
),
(r'MD5("abc")', Literal("900150983cd24fb0d6963f7d28e17f72")),
(r'SHA1("abc")', Literal("a9993e364706816aba3e25717850c26c9cd0d89d")),
(
r'SHA256("abc")',
Literal("ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"),
),
(
r'SHA384("abc")',
Literal(
"cb00753f45a35e8bb5a03d699ac65007272c32ab0eded1631a8b605a43ff5bed8086072ba1e7cc2358baeca134c825a7"
),
),
(
r'SHA512("abc")',
Literal(
"ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f"
),
),
],
)
def test_function(expression: str, expected_result: Identifier) -> None:
graph = Graph()
query_string = """
PREFIX eg: <https://example.com/>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
CONSTRUCT { eg:subject eg:predicate ?o }
WHERE {
BIND(???EXPRESSION_PLACEHOLDER??? AS ?o)
}
""".replace(
"???EXPRESSION_PLACEHOLDER???", expression
)
result = graph.query(query_string)
assert result.type == "CONSTRUCT"
assert isinstance(result.graph, Graph)
logging.debug("result = %s", list(result.graph.triples((None, None, None))))
actual_result = result.graph.value(EG.subject, EG.predicate, any=False)
if isinstance(expected_result, type):
assert isinstance(actual_result, expected_result)
else:
assert expected_result == actual_result


@pytest.mark.parametrize(
["literal", "range", "expected_result"],
[
(Literal("en"), Literal("en"), True),
(Literal("en"), Literal("EN"), True),
(Literal("EN"), Literal("en"), True),
(Literal("EN"), Literal("EN"), True),
(Literal("en"), Literal("en-US"), False),
(Literal("en-US"), Literal("en-US"), True),
(Literal("en-gb"), Literal("en-GB"), True),
],
)
def test_lang_range_check(
literal: Literal, range: Literal, expected_result: bool
) -> None:
actual_result = _lang_range_check(range, literal)
assert expected_result == actual_result

0 comments on commit af17916

Please sign in to comment.