# Copyright (C) 2024 IBM Corp.
# SPDX-License-Identifier: Apache-2.0
from __future__ import annotations
import datetime
import enum
from ...typing import Any, ClassVar, Location, override, Self, TypeAlias, Union
from ..term import Template, Variable
from .deep_data_value import (
DeepDataValue,
DeepDataValueTemplate,
DeepDataValueVariable,
)
from .item import Item, ItemTemplate, ItemVariable, VItem, VTItem
from .quantity import Quantity, QuantityVariable, TQuantity, VTQuantityContent
from .string import String
from .value import Datatype
TDatetime: TypeAlias = Union[datetime.datetime, datetime.date, str]
TTime: TypeAlias = Union['Time', TDatetime]
VTime: TypeAlias = Union['TimeTemplate', 'TimeVariable', 'Time']
VTimeContent: TypeAlias = Union['TimeVariable', datetime.datetime]
VTTimeContent: TypeAlias = Union[Variable, TTime]
TTimePrecision: TypeAlias = Union['Time.Precision', TQuantity]
VTimePrecisionContent: TypeAlias = Union[QuantityVariable, 'Time.Precision']
VTTimePrecisionContent: TypeAlias = Union[Variable, TTimePrecision]
TTimeTimezone: TypeAlias = TQuantity
VTimeTimezoneContent: TypeAlias = Union[QuantityVariable, int]
VTTimeTimezoneContent: TypeAlias = VTQuantityContent
[docs]
class TimeTemplate(DeepDataValueTemplate):
"""Time template.
Parameters:
time: Time or time variable.
precision: Precision or quantity variable.
timezone: Time zone or quantity variable.
calendar: Calendar model, item template, or item variable.
"""
object_class: ClassVar[type[Time]] # pyright: ignore
[docs]
def __init__(
self,
time: VTTimeContent,
precision: VTTimePrecisionContent | None = None,
timezone: VTTimeTimezoneContent | None = None,
calendar: VTItem | None = None
) -> None:
super().__init__(time, precision, timezone, calendar)
@override
def _preprocess_arg(self, arg: Any, i: int) -> Any:
if i == 1: # time
if isinstance(arg, Variable):
return TimeVariable.check(arg, type(self), None, i)
else:
return Time._static_preprocess_arg(self, arg, i)
elif i == 2: # precision
if isinstance(arg, Variable):
return QuantityVariable.check(arg, type(self), None, i)
else:
return Time._static_preprocess_arg(self, arg, i)
elif i == 3: # timezone
if isinstance(arg, Variable):
return QuantityVariable.check(arg, type(self), None, i)
else:
return Time._static_preprocess_arg(self, arg, i)
elif i == 4: # calendar
if isinstance(arg, Template):
return ItemTemplate.check(arg, type(self), None, i)
elif isinstance(arg, Variable):
return ItemVariable.check(arg, type(self), None, i)
else:
return Time._static_preprocess_arg(self, arg, i)
else:
raise self._should_not_get_here()
@property
def time(self) -> VTimeContent:
"""The date-time of time template."""
return self.get_time()
[docs]
def get_time(self) -> VTimeContent:
"""Gets the date-time of time.
Returns:
Date-Time or time variable.
"""
return self.args[0]
@property
def precision(self) -> VTimePrecisionContent | None:
"""The precision of time template."""
return self.get_precision()
[docs]
def get_precision(
self,
default: VTimePrecisionContent | None = None
) -> VTimePrecisionContent | None:
"""Gets the precision of time.
If the precision is ``None``, returns `default`.
Parameters:
default: Default precision.
Returns:
Precision or quantity variable.
"""
return self.get(1, default)
@property
def timezone(self) -> VTimeTimezoneContent | None:
"""The timezone of time template."""
return self.get_timezone()
[docs]
def get_timezone(
self,
default: VTimeTimezoneContent | None = None
) -> VTimeTimezoneContent | None:
"""Gets the timezone of time template
If the timezone is ``None``, returns `default`.
Parameters:
default: Default timezone.
Returns:
Timezone or quantity variable.
"""
return self.get(2, default)
@property
def calendar(self) -> VItem | None:
"""The calendar model of time template."""
return self.get_calendar()
[docs]
def get_calendar(
self,
default: VItem | None = None
) -> VItem | None:
"""Gets calendar model of time template.
If the calendar model is ``None``, returns `default`.
Parameters:
default: Default calendar model.
Returns:
Calendar model, item template, or item variable.
"""
return self.get(3, default)
[docs]
class TimeVariable(DeepDataValueVariable):
"""Time variable.
Parameters:
name: Name.
"""
object_class: ClassVar[type[Time]] # pyright: ignore
class TimeDatatype(Datatype):
"""Time datatype."""
value_class: ClassVar[type[Time]] # pyright: ignore
[docs]
class Time(
DeepDataValue,
datatype_class=TimeDatatype,
template_class=TimeTemplate,
variable_class=TimeVariable
):
"""Time.
Parameters:
time: Time.
precision: Precision.
timezone: Time zone.
calendar: Calendar model.
"""
datatype_class: ClassVar[type[TimeDatatype]] # pyright: ignore
datatype: ClassVar[TimeDatatype] # pyright: ignore
template_class: ClassVar[type[TimeTemplate]] # pyright: ignore
variable_class: ClassVar[type[TimeVariable]] # pyright: ignore
# See:
# <https://www.mediawiki.org/wiki/Wikibase/Indexing/RDF_Dump_Format#Time>.
# <https://www.mediawiki.org/wiki/Wikibase/DataModel#Dates_and_times>.
[docs]
class Precision(enum.Enum):
"""Time precision."""
#: Billion years.
BILLION_YEARS = 0
#: Hundred million years.
HUNDRED_MILLION_YEARS = 1
#: Ten million years.
TEN_MILLION_YEARS = 2
#: Million years.
MILLION_YEARS = 3
#: Hundred thousand years.
HUNDRED_THOUSAND_YEARS = 4
#: Ten thousand years.
TEN_THOUSAND_YEARS = 5
#: Millennia.
MILLENNIA = 6
#: Century.
CENTURY = 7
#: Decade.
DECADE = 8
#: Year.
YEAR = 9
#: Month.
MONTH = 10
#: Day.
DAY = 11
#: Hour.
HOUR = 12
#: Minute.
MINUTE = 13
#: Second.
SECOND = 14
#: Billion years.
BILLION_YEARS = Precision.BILLION_YEARS
#: Hundred million years.
HUNDRED_MILLION_YEARS = Precision.HUNDRED_MILLION_YEARS
#: Ten million years.
TEN_MILLION_YEARS = Precision.TEN_MILLION_YEARS
#: Million years.
MILLION_YEARS = Precision.MILLION_YEARS
#: Hundred thousand years.
HUNDRED_THOUSAND_YEARS = Precision.HUNDRED_THOUSAND_YEARS
#: Ten thousand years.
TEN_THOUSAND_YEARS = Precision.TEN_THOUSAND_YEARS
#: Millennia.
MILLENNIA = Precision.MILLENNIA
#: Century.
CENTURY = Precision.CENTURY
#: Decade.
DECADE = Precision.DECADE
#: Year.
YEAR = Precision.YEAR
#: Month.
MONTH = Precision.MONTH
#: Day.
DAY = Precision.DAY
#: Hour.
HOUR = Precision.HOUR
#: Minute.
MINUTE = Precision.MINUTE
#: Second.
SECOND = Precision.SECOND
[docs]
@classmethod
@override
def check(
cls,
arg: Any,
function: Location | None = None,
name: str | None = None,
position: int | None = None
) -> Self:
if isinstance(arg, cls):
return arg
elif isinstance(arg, String):
return cls(arg.content)
elif isinstance(arg, (datetime.datetime, datetime.date, str)):
return cls(arg)
else:
raise cls._check_error(arg, function, name, position)
[docs]
def __init__(
self,
time: VTTimeContent,
precision: VTTimePrecisionContent | None = None,
timezone: VTTimeTimezoneContent | None = None,
calendar: VTItem | None = None
) -> None:
super().__init__(time, precision, timezone, calendar)
@override
def _preprocess_arg(self, arg: Any, i: int) -> Any:
return self._static_preprocess_arg(self, arg, i)
@staticmethod
def _static_preprocess_arg(self_, arg: Any, i: int) -> Any:
if arg is None and 2 <= i <= 4:
return None
if i == 1: # time
if isinstance(arg, Time):
return arg.time
elif isinstance(arg, str):
###
# FIXME: Python's fromisoformat() does not support the +/-
# sign used by Wikidata at the start of date-time literals.
###
arg = arg[1:] if arg[0] == '+' or arg[0] == '-' else arg
try:
dt = datetime.datetime.fromisoformat(arg)
except ValueError as err:
raise Time._check_error(
arg, type(self_), None, i, ValueError) from err
if dt.tzinfo is None:
return dt.replace(tzinfo=datetime.timezone.utc)
else:
return dt
elif isinstance(arg, datetime.datetime):
return arg
elif isinstance(arg, datetime.date):
return datetime.datetime.combine(
arg, datetime.time(), tzinfo=datetime.timezone.utc)
else:
raise Time._check_error(arg, type(self_), None, i)
elif i == 2: # precision
if isinstance(arg, Time.Precision):
return arg
else:
try:
return Time.Precision(
Quantity.check(arg, type(self_), None, i).amount)
except ValueError as err:
raise Time._check_error(
arg, type(self_), None, i,
to_=Time.Precision.__qualname__) from err
elif i == 3: # timezone
if isinstance(arg, int):
return arg
else:
return int(Quantity.check(arg, type(self_), None, i).amount)
elif i == 4: # calendar
return Item.check(arg, type(self_), None, i)
else:
raise self_._should_not_get_here()
@property
def time(self) -> datetime.datetime:
"""The date-time of time."""
return self.get_time()
[docs]
def get_time(self) -> datetime.datetime:
"""Gets the date-time of time.
Returns:
Datetime.
"""
return self.args[0]
@property
def precision(self) -> Precision | None:
"""The precision of time."""
return self.get_precision()
[docs]
def get_precision(
self,
default: Time.Precision | None = None
) -> Precision | None:
"""Gets the precision of time.
If the precision is ``None``, returns `default`.
Parameters:
default: Default precision.
Returns:
Precision.
"""
return self.get(1, default)
@property
def timezone(self) -> int | None:
"""The timezone of time."""
return self.get_timezone()
[docs]
def get_timezone(self, default: int | None = None) -> int | None:
"""Gets the timezone of time.
If the timezone is ``None``, returns `default`.
Parameters:
default: Default timezone.
Returns:
Timezone.
"""
return self.get(2, default)
@property
def calendar(self) -> Item | None:
"""The calendar model of time."""
return self.get_calendar()
[docs]
def get_calendar(self, default: Item | None = None) -> Item | None:
"""Gets calendar model of time.
If the calendar model is ``None``, returns `default`.
Parameters:
default: Default calendar model.
Returns:
Calendar model.
"""
return self.get(3, default)