import abc
from collections import OrderedDict
from bibtexparser.library import Library
from bibtexparser.model import Block
from bibtexparser.model import Entry
from bibtexparser.model import Field
from .middleware import BlockMiddleware
class _MonthInterpolator(BlockMiddleware, abc.ABC):
"""Abstract class to handle month-conversions."""
# docstr-coverage: inherited
def __init__(self, allow_inplace_modification: bool = True):
super().__init__(
allow_inplace_modification=allow_inplace_modification,
allow_parallel_execution=True,
)
# docstr-coverage: inherited
def transform_entry(self, entry: Entry, library: "Library") -> Block:
try:
month = entry.fields_dict["month"]
except KeyError:
return entry
new_val, meta = self.resolve_month_field_val(month)
# Assigning `Field.value` resets the `enclosing` demand references rely on.
if type(new_val) is not type(month.value) or new_val != month.value:
month.value = new_val
demanded_enclosing = self._demanded_enclosing(new_val)
if demanded_enclosing is not None:
month.enclosing = demanded_enclosing
entry.parser_metadata[self.metadata_key()] = meta
return entry
@abc.abstractmethod
def resolve_month_field_val(self, month_field: Field) -> tuple[str | int, str]:
"""Transform the month field.
Args:
month_field: The month field to transform.
Returns:
A tuple of the transformed value and the metadata."""
raise NotImplementedError("Abstract method")
def _demanded_enclosing(self, new_value: str | int) -> str | None:
"""The `Field.enclosing` to demand for the transformed month value, if any."""
return None
_MONTH_ABBREV_TO_FULL = OrderedDict(
[
("jan", "January"),
("feb", "February"),
("mar", "March"),
("apr", "April"),
("may", "May"),
("jun", "June"),
("jul", "July"),
("aug", "August"),
("sep", "September"),
("oct", "October"),
("nov", "November"),
("dec", "December"),
]
)
_MONTH_ABBREV = list(_MONTH_ABBREV_TO_FULL.keys())
_MONTH_FULL = list(_MONTH_ABBREV_TO_FULL.values())
_LOWERCASE_FULL = list(m.lower() for m in _MONTH_FULL)
[docs]
class MonthLongStringMiddleware(_MonthInterpolator):
"""Replace month numbers with full month names.
Note that this may be used before or after removing the enclosing,
but the semantics are different: Enclosed values (e.g. '{jan}', '"jan"' or '"1"')
will not be transformed. If you want to transform these values, you should
use this middleware after the RemoveEnclosingMiddleware.
Full names returned by this middleware are always capitalized
and unenclosed."""
# docstr-coverage: inherited
# docstr-coverage: inherited
[docs]
def resolve_month_field_val(self, month_field: Field):
v = month_field.value
if isinstance(v, str) and v.isdigit():
v = int(v)
if isinstance(v, int):
if v < 1 or v > 12:
return (
month_field.value,
f"month-field unchanged - unknown month {v}",
) # Nothing we can do here
return _MONTH_FULL[v - 1], "transformed int-month to str-month"
elif isinstance(v, str):
v_lower = v.lower()
if v_lower in _MONTH_ABBREV_TO_FULL:
return (
_MONTH_ABBREV_TO_FULL[v_lower],
"transformed abbreviated month to full month",
)
elif v_lower in _LOWERCASE_FULL:
default_casing = _MONTH_ABBREV_TO_FULL[v_lower[:3]]
if v != default_casing:
return default_casing, "transformed month casing"
# We can't do anything else here
return month_field.value, "month field unchanged"
[docs]
class MonthAbbreviationMiddleware(_MonthInterpolator):
"""Replace month values with month abbreviations.
Note that this may be used before or after removing the enclosing,
but the semantics are different: Enclosed values (e.g. '{jan}', '"jan"' or '"1"')
will not be transformed. If you want to transform these values, you should
use this middleware after the RemoveEnclosingMiddleware.
The created abbreviations are always lowercase and unenclosed.
As the abbreviations are bibtex string references (macros, e.g. `jan`),
a `no-enclosing` demand is set on the month field (see `Field.enclosing`),
making sure the `AddEnclosingMiddleware` keeps them unenclosed when writing."""
# docstr-coverage: inherited
# docstr-coverage: inherited
def _demanded_enclosing(self, new_value: str | int) -> str | None:
# Month macros (jan, feb, ...) must not be enclosed when written,
# as that would turn them into plain strings instead of references.
if isinstance(new_value, str) and new_value in _MONTH_ABBREV:
return "no-enclosing"
return None
# docstr-coverage: inherited
[docs]
def resolve_month_field_val(self, month_field: Field):
v = month_field.value
if isinstance(v, str) and v.isdigit():
v = int(v)
if isinstance(v, int):
if v < 1 or v > 12:
# Nothing we can do here
return month_field.value, f"month-field unchanged - unknown month {v}"
return _MONTH_ABBREV[v - 1], "transformed int-month to abbreviated month"
elif isinstance(v, str):
v_lower = v.lower()
if v_lower in _LOWERCASE_FULL:
return v_lower[:3], "transformed full month to abbreviated month"
elif v_lower in _MONTH_ABBREV and not v_lower == v:
return v_lower, "use lowercase month abbreviation"
return month_field.value, "month field unchanged"
[docs]
class MonthIntMiddleware(_MonthInterpolator):
"""Replace month values with month numbers.
Note that this may be used before or after removing the enclosing,
but the semantics are different: Enclosed values (e.g. '{jan}', '"jan"' or '"1"')
will not be transformed. If you want to transform these values, you should
use this middleware after the RemoveEnclosingMiddleware.
The created int-months are always integers and unenclosed.
A `no-enclosing` demand is set on the month field (see `Field.enclosing`),
making sure the `AddEnclosingMiddleware` keeps them unenclosed when writing."""
# docstr-coverage: inherited
# docstr-coverage: inherited
def _demanded_enclosing(self, new_value: str | int) -> str | None:
if isinstance(new_value, int):
return "no-enclosing"
return None
# docstr-coverage: inherited
[docs]
def resolve_month_field_val(self, month_field: Field):
v = month_field.value
if isinstance(v, str):
v_lower = v.lower()
if v_lower in _MONTH_ABBREV:
return (
_MONTH_ABBREV.index(v_lower[:3]) + 1,
"transformed abbreviated month to int-month",
)
elif v_lower in _LOWERCASE_FULL:
return (
_LOWERCASE_FULL.index(v_lower) + 1,
"transformed full month to int-month",
)
if isinstance(v, str) and v.isdigit():
if 1 <= int(v) <= 12:
return int(v), "cast month int-string to int"
return month_field.value, "month field unchanged"