Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 33 additions & 12 deletions lib/query_processing/src/expressions/functions/round_.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::errors::QueryProcessingError;
use polars::prelude::{as_struct, col, lit, LiteralValue, RoundMode};
use oxrdf::vocab::xsd;
use polars::prelude::{as_struct, col, lit, when, Expr, LiteralValue, RoundMode};
use representation::query_context::Context;
use representation::solution_mapping::SolutionMappings;
use representation::{BaseRDFNodeType, RDFNodeState};
Expand All @@ -21,11 +22,6 @@ pub fn round_(
));
}
let first_context = args_contexts.get(&0).unwrap();
solution_mappings.mappings = solution_mappings.mappings.with_column(
col(first_context.as_str())
.round(0, RoundMode::HalfAwayFromZero)
.alias(outer_context.as_str()),
);
let existing_type = solution_mappings
.rdf_node_types
.get(first_context.as_str())
Expand All @@ -36,17 +32,22 @@ pub fn round_(
for t in existing_type.map.keys() {
if t.is_numeric() {
rounded_exprs.push(
col(first_context.as_str())
.struct_()
.field_by_name(&t.field_col_name())
.round(0, RoundMode::HalfAwayFromZero)
.alias(&t.field_col_name()),
sparql_round(
col(first_context.as_str())
.struct_()
.field_by_name(&t.field_col_name()),
t,
)
.alias(&t.field_col_name()),
);
rounded_types.push(t.clone());
}
}
} else if existing_type.is_numeric() {
rounded_exprs.push(col(first_context.as_str()).round(0, RoundMode::HalfAwayFromZero));
rounded_exprs.push(sparql_round(
col(first_context.as_str()),
existing_type.get_base_type().unwrap(),
));
rounded_types.push(existing_type.get_base_type().unwrap().clone());
}
if rounded_exprs.is_empty() {
Expand Down Expand Up @@ -86,3 +87,23 @@ pub fn round_(
}
Ok(solution_mappings)
}

/// SPARQL 1.1 17.4.4.2 (fn:numeric-round): returns the number with no fractional part
/// that is closest to the argument. If there are two such numbers, the one closest to
/// positive infinity is returned, e.g. round(-2.5) = -2.
///
/// Polars has no such rounding mode, so we round half away from zero and move negative
/// ties one step up. For a tie, x - round(x) is exactly 0.5 (also for floats), so the
/// comparison is exact.
fn sparql_round(e: Expr, t: &BaseRDFNodeType) -> Expr {
let rounded = e.clone().round(0, RoundMode::HalfAwayFromZero);
if t.is_lit_type(xsd::DECIMAL) || t.is_lit_type(xsd::DOUBLE) || t.is_lit_type(xsd::FLOAT) {
let dt = t.default_input_polars_data_type();
when((e - rounded.clone()).eq(lit(0.5).cast(dt.clone())))
.then(rounded.clone() + lit(1).cast(dt))
.otherwise(rounded)
} else {
// Integer types have no fractional part
rounded
}
}
43 changes: 43 additions & 0 deletions py_maplib/tests/test_round.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import pytest

from maplib import Model

# SPARQL 1.1 17.4.4.2: "Returns the number with no fractional part that is closest to the
# argument. If there are two such numbers, then the one that is closest to positive
# infinity is returned." Examples: round(2.5) = 3.0, round(-2.5) = -2.0, round(2.4999) = 2.0


@pytest.mark.parametrize(
"expression,expected",
[
("ROUND(2.5)", 3),
("ROUND(-2.5)", -2),
("ROUND(2.4999)", 2),
("ROUND(-0.5)", 0),
("ROUND(-36.5)", -36),
("ROUND(-36.6)", -37),
("ROUND(-36.4)", -36),
("ROUND(2.5e0)", 3),
("ROUND(-2.5e0)", -2),
("ROUND(-2.6e0)", -3),
("ROUND(-3)", -3),
("ROUND(4)", 4),
],
)
def test_round_ties_towards_positive_infinity(expression, expected):
m = Model()
df = m.query(f"SELECT ({expression} AS ?r) WHERE {{}}")
assert float(df["r"][0]) == expected


def test_round_on_data():
m = Model()
m.insert(
"""PREFIX ex: <http://example.org/>
CONSTRUCT { ex:a ex:v -36.5 . ex:b ex:v 36.5 . ex:c ex:v -36.51 . } WHERE {}"""
)
df = m.query(
"""PREFIX ex: <http://example.org/>
SELECT ?x (ROUND(?v) AS ?r) WHERE { ?x ex:v ?v } ORDER BY ?x"""
)
assert [float(x) for x in df["r"].to_list()] == [-36, 37, -37]