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
11 changes: 10 additions & 1 deletion pygeofilter/parsers/cql2_json/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,20 @@ def walk_cql_json(node: JsonType): # noqa: C901
return parse_datetime(node["timestamp"])

elif "interval" in node:
parsed: List[Union[date, datetime, timedelta, None]] = []
parsed: List[Union[date, datetime, timedelta, ast.Node, None]] = []
for value in node["interval"]:
if value == "..":
parsed.append(None)
continue
if not isinstance(value, str):
# An interval bound need not be a date/time literal: CQL2 also
# allows a property reference, e.g.
# ``{"interval": [{"property": "start"}, {"property": "end"}]}``
# (conformance test 41). Walk it like any other expression and
# let the evaluator resolve it; the split() below only accepts
# strings.
parsed.append(walk_cql_json(value))
continue
try:
parsed.append(parse_date(value))
except ValueError:
Expand Down
13 changes: 10 additions & 3 deletions pygeofilter/values.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,15 @@

from dataclasses import dataclass
from datetime import date, datetime, time, timedelta
from typing import Any, List, Optional, Union
from typing import TYPE_CHECKING, Any, List, Optional, Union

from pygeoif import shape

if TYPE_CHECKING:
# ``ast`` imports this module at runtime, so the dependency is one-way;
# type checkers still need the name for ``Interval`` below.
from . import ast


@dataclass
class Geometry:
Expand Down Expand Up @@ -77,8 +82,10 @@ def __eq__(self, o: object) -> bool:

@dataclass
class Interval:
start: Optional[Union[date, datetime, timedelta]] = None
end: Optional[Union[date, datetime, timedelta]] = None
# A bound is either a literal (date/time/duration) or, as the CQL2
# conformance tests allow, an expression such as a property reference.
start: Optional[Union[date, datetime, timedelta, "ast.Node"]] = None
end: Optional[Union[date, datetime, timedelta, "ast.Node"]] = None

def get_sub_nodes(self) -> List[Any]: # TODO: find way to type this
return [self.start, self.end]
Expand Down
90 changes: 90 additions & 0 deletions tests/parsers/cql2_json/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -745,3 +745,93 @@ def test_function_attr_string_arg():
],
),
)


def test_interval_with_property_bounds():
# #116 / CQL2 conformance test 41: an interval bound may be a property
result = parse(
{
"op": "t_contains",
"args": [
{
"interval": [
{"property": "start_datetime"},
{"property": "end_datetime"},
]
},
{"interval": ["2000-01-01T00:00:00Z", "2000-01-01T00:00:01Z"]},
],
}
)
assert result == ast.TimeContains(
values.Interval(
ast.Attribute("start_datetime"), ast.Attribute("end_datetime")
),
values.Interval(
datetime(
2000, 1, 1, 0, 0, 0, tzinfo=StaticTzInfo("Z", timedelta(0))
),
datetime(
2000, 1, 1, 0, 0, 1, tzinfo=StaticTzInfo("Z", timedelta(0))
),
),
)


def test_interval_with_property_and_open_end():
result = parse(
{
"op": "t_during",
"args": [
{"property": "attr"},
{"interval": [{"property": "start_datetime"}, ".."]},
],
}
)
assert result == ast.TimeDuring(
ast.Attribute("attr"),
values.Interval(ast.Attribute("start_datetime"), None),
)


def test_interval_with_open_start_and_property():
result = parse(
{
"op": "t_during",
"args": [
{"property": "attr"},
{"interval": ["..", {"property": "end_datetime"}]},
],
}
)
assert result == ast.TimeDuring(
ast.Attribute("attr"),
values.Interval(None, ast.Attribute("end_datetime")),
)


def test_interval_with_function_bound():
result = parse(
{
"op": "t_during",
"args": [
{"property": "attr"},
{
"interval": [
{
"function": {
"name": "myfunc",
"arguments": [{"property": "start_datetime"}],
}
},
"2000-01-01T00:00:01Z",
]
},
],
}
)
assert isinstance(result, ast.TimeDuring)
assert isinstance(result.rhs, values.Interval)
assert result.rhs.start == ast.Function(
"myfunc", [ast.Attribute("start_datetime")]
)