Skip to content

Justice

A Justice, as defined in this reference document, is one of many justices sitting in the Supreme Court:

Bases: BaseModel

Justice

Field Type Description
id int Unique identifier of the Justice based on appointment roster
full_name str First + last + suffix
first_name str -
last_name str -
suffix str e.g. Jr., Sr., III, etc.
nick_name str -
gender str -
alias str Other names
start_term str Time justice appointed
end_term str Time justice
chief_date str Date appointed as Chief Justice (optional)
birth_date str Date of birth
retire_date str Based on the Birth Date, if it exists, it is the maximum term of service allowed by law.
inactive_date str Which date is earliest inactive date of the Justice, the retire date is set automatically but it is not guaranteed to to be the actual inactive date. So the inactive date is either that specified in the end_term or the retire_date, whichever is earlier.

The list of justices from the sc.yaml file are parsed through this model prior to being inserted into the database.

Source code in corpus_judge/justice_model.py
Python
class Justice(BaseModel):
    """
    # Justice

    Field | Type | Description
    --:|:--|:--
    id |int | Unique identifier of the Justice based on appointment roster
    full_name |str | First + last + suffix
    first_name |str | -
    last_name |str | -
    suffix |str | e.g. Jr., Sr., III, etc.
    nick_name |str | -
    gender |str | -
    alias |str | Other names
    start_term |str | Time justice appointed
    end_term |str | Time justice
    chief_date |str | Date appointed as Chief Justice (optional)
    birth_date |str | Date of birth
    retire_date |str | Based on the Birth Date, if it exists, it is the maximum term of service allowed by law.
    inactive_date |str | Which date is earliest inactive date of the Justice, the retire date is set automatically but it is not guaranteed to to be the actual inactive date. So the inactive date is either that specified in the `end_term` or the `retire_date`, whichever is earlier.

    The list of justices from the sc.yaml file are parsed through this model prior to being inserted
    into the database.
    """  # noqa: E501

    model_config = ConfigDict(use_enum_values=True)
    id: int = Field(
        ...,
        title="Justice ID Identifier",
        description=(
            "Starting from 1, the integer represents the order of appointment"
            " to the Supreme Court."
        ),
        ge=1,
        lt=1000,
    )
    full_name: str | None = Field(None)
    first_name: str = Field(..., max_length=50)
    last_name: str = Field(..., max_length=50)
    suffix: Suffix | None = Field(None, max_length=4)
    gender: Gender = Field(...)
    alias: str | None = Field(
        None,
        title="Alias",
        description="Means of matching ponente and voting strings to the justice id.",
    )
    start_term: datetime.date | None = Field(
        None,
        title="Start Term",
        description="Date of appointment.",
    )
    end_term: datetime.date | None = Field(
        None,
        title="End Term",
        description="Date of termination.",
    )
    chief_date: datetime.date | None = Field(
        None,
        title="Date Appointed As Chief Justice",
        description=(
            "When appointed, the extension title of the justice changes from"
            " 'J.' to 'C.J'. for cases that are decided after the date of"
            " appointment but before the date of retirement."
        ),
    )
    birth_date: datetime.date | None = Field(
        None,
        title="Date of Birth",
        description=(
            "The Birth Date is used to determine the retirement age of the"
            " justice. Under the 1987 constitution, this is"
            f" {MAX_JUSTICE_AGE}. There are missing dates: see Jose Generoso"
            " 41, Grant Trent 14, Fisher 19, Moir 20."
        ),
    )
    retire_date: datetime.date | None = Field(
        None,
        title="Mandatory Retirement Date",
        description=(
            "Based on the Birth Date, if it exists, it is the maximum term of"
            " service allowed by law."
        ),
    )
    inactive_date: datetime.date | None = Field(
        None,
        title="Date",
        description=(
            "Which date is earliest inactive date of the Justice, the retire"
            " date is set automatically but it is not guaranteed to to be the"
            " actual inactive date. So the inactive date is either that"
            " specified in the `end_term` or the `retire_date`, whichever is"
            " earlier."
        ),
    )

    @field_validator("retire_date")
    def retire_date_70_years(cls, v, values):
        if v and values["birth_date"]:
            if values["birth_date"] + rd(years=MAX_JUSTICE_AGE) != v:
                raise ValueError("Must be 70 years from birth date.")
        return v

    @classmethod
    def from_data(cls, data: dict):
        def extract_date(text: str | None) -> datetime.date | None:
            return parse(text).date() if text else None

        # Not all justices have/need aliases; default needed
        alias = data.pop("Alias", None)
        if not alias:
            if surname := data.get("last_name"):
                if suffix := data.get("suffix"):
                    alias = f"{surname} {suffix}".lower()

        retire_date = None
        if dob := extract_date(data.pop("Born")):
            retire_date = dob + rd(years=MAX_JUSTICE_AGE)

        # retire_date = latest date allowed; but if end_date present, use this
        inactive_date = retire_date
        if end_date := extract_date(data.pop("End of term")):
            inactive_date = end_date or retire_date

        return cls(
            id=data.pop("#"),
            full_name=data["full_name"],
            first_name=data["first_name"],
            last_name=data["last_name"],
            suffix=Suffix(data["suffix"]),
            gender=Gender(data["gender"]),
            alias=alias,
            birth_date=dob,
            start_term=extract_date(data.pop("Start of term")),
            end_term=end_date,
            chief_date=extract_date(data.pop("Appointed chief")),
            retire_date=retire_date,
            inactive_date=inactive_date,
        )

Creating a Justice table

The source file for the sqlite database is sc.yaml. This ought to be edited whenever new justices are added. It is referenced via JUSTICE_FILE:

Note the default table name used by CandidateJustice is "justices".

Python
from corpus_judge import JUSTICE_FILE
from sqlite_utils import Database
from pathlib import Path
import yaml

def add_justices(db: Database):
    tbl = db["justices"] # default table name
    if not tbl.exists():
        file_content = JUSTICE_FILE.read_bytes()
        records = yaml.safe_load(file_content)
        tbl.insert_all(records, ignore=True)  # type: ignore

p = Path("new.db")
db = Database(p)
add_justices(db)

Cleaning Raw Justice Names

Bases: NamedTuple

Source code in corpus_judge/justice_name.py
Python
class OpinionWriterName(NamedTuple):
    writer: str | None = None
    per_curiam: bool = False

    @classmethod
    def extract(cls, text: str | None) -> Self | None:
        """Will mark `per_curiam` to be True if the regex pattern matches,
        else, will clean the writer represented by the text, if possible.

        Examples:
            >>> OpinionWriterName.extract('Justice Marvic M.V.F. Leonen')
            OpinionWriterName(writer='leonen', per_curiam=False)
            >>> OpinionWriterName.extract('Justice Filomena D. Signh') # note bad spelling
            OpinionWriterName(writer='singh', per_curiam=False)

        Args:
            text (str | None): Text to evaluate.

        Returns:
            Self | None: Instance representing the writer.
        """  # noqa: E501
        if not text:
            return None
        if text:
            if IS_PER_CURIAM.search(text):
                return cls(per_curiam=True)
            # if text is modern, e.g. from the 2023 SC website, adjust the text
            # prior to clean() since the clean function was intended for the
            # more traditional elibrary.
            text = limit_modern_to_terminal_text(text)
            writer = cls.clean(text)  # check proper
            return cls(writer=writer)

    @classmethod
    def clean(cls, text: str) -> str | None:
        """Each `ponente` name stored in the database can be uniform. Will
        parse text, apply some cleaning steps, and result in a lower-cased form
        of the original `text`, e.g.:

        Some constraints:

        1. Must be more than 4 characters
        2. Must be less than 40 characters

        Examples:
            >>> OpinionWriterName.clean("REYES , J.B.L, Acting C.J.") # sample name 1
            'reyes, j.b.l.'
            >>> OpinionWriterName.clean("REYES, J, B. L. J.") # sample name 2
            'reyes, j.b.l.'
        """

        no_asterisk = re.sub(r"\[?(\*)+\]?", "", text)
        name = initialize_name(no_asterisk)
        no_suffix = TitleSuffix.cull(name).strip()
        repl = CommonTypos.replace(no_suffix).strip()
        res = repl + "." if repl.endswith((" jr", " sr")) else repl
        return res if 4 < len(res) < 40 else None

Functions

clean(text) classmethod

Each ponente name stored in the database can be uniform. Will parse text, apply some cleaning steps, and result in a lower-cased form of the original text, e.g.:

Some constraints:

  1. Must be more than 4 characters
  2. Must be less than 40 characters

Examples:

Python Console Session
>>> OpinionWriterName.clean("REYES , J.B.L, Acting C.J.") # sample name 1
'reyes, j.b.l.'
>>> OpinionWriterName.clean("REYES, J, B. L. J.") # sample name 2
'reyes, j.b.l.'
Source code in corpus_judge/justice_name.py
Python
@classmethod
def clean(cls, text: str) -> str | None:
    """Each `ponente` name stored in the database can be uniform. Will
    parse text, apply some cleaning steps, and result in a lower-cased form
    of the original `text`, e.g.:

    Some constraints:

    1. Must be more than 4 characters
    2. Must be less than 40 characters

    Examples:
        >>> OpinionWriterName.clean("REYES , J.B.L, Acting C.J.") # sample name 1
        'reyes, j.b.l.'
        >>> OpinionWriterName.clean("REYES, J, B. L. J.") # sample name 2
        'reyes, j.b.l.'
    """

    no_asterisk = re.sub(r"\[?(\*)+\]?", "", text)
    name = initialize_name(no_asterisk)
    no_suffix = TitleSuffix.cull(name).strip()
    repl = CommonTypos.replace(no_suffix).strip()
    res = repl + "." if repl.endswith((" jr", " sr")) else repl
    return res if 4 < len(res) < 40 else None

extract(text) classmethod

Will mark per_curiam to be True if the regex pattern matches, else, will clean the writer represented by the text, if possible.

Examples:

Python Console Session
>>> OpinionWriterName.extract('Justice Marvic M.V.F. Leonen')
OpinionWriterName(writer='leonen', per_curiam=False)
>>> OpinionWriterName.extract('Justice Filomena D. Signh') # note bad spelling
OpinionWriterName(writer='singh', per_curiam=False)

Parameters:

Name Type Description Default
text str | None

Text to evaluate.

required

Returns:

Type Description
Self | None

Self | None: Instance representing the writer.

Source code in corpus_judge/justice_name.py
Python
@classmethod
def extract(cls, text: str | None) -> Self | None:
    """Will mark `per_curiam` to be True if the regex pattern matches,
    else, will clean the writer represented by the text, if possible.

    Examples:
        >>> OpinionWriterName.extract('Justice Marvic M.V.F. Leonen')
        OpinionWriterName(writer='leonen', per_curiam=False)
        >>> OpinionWriterName.extract('Justice Filomena D. Signh') # note bad spelling
        OpinionWriterName(writer='singh', per_curiam=False)

    Args:
        text (str | None): Text to evaluate.

    Returns:
        Self | None: Instance representing the writer.
    """  # noqa: E501
    if not text:
        return None
    if text:
        if IS_PER_CURIAM.search(text):
            return cls(per_curiam=True)
        # if text is modern, e.g. from the 2023 SC website, adjust the text
        # prior to clean() since the clean function was intended for the
        # more traditional elibrary.
        text = limit_modern_to_terminal_text(text)
        writer = cls.clean(text)  # check proper
        return cls(writer=writer)

Candidate Justice

Bases: NamedTuple

Source code in corpus_judge/justice_select.py
Python
class CandidateJustice(NamedTuple):
    db: Database
    text: str | None = None
    date_str: str | None = None
    tablename: str = "justices"

    @property
    def valid_date(self) -> datetime.date | None:
        if not self.date_str:
            return None
        try:
            return parse(self.date_str).date()
        except Exception:
            return None

    @property
    def src(self):
        return OpinionWriterName.extract(self.text)

    @property
    def candidate(self) -> str | None:
        return self.src and self.src.writer

    @property
    def table(self) -> Table:
        res = self.db[self.tablename]
        if isinstance(res, Table):
            return res
        raise Exception("Not a valid table.")

    @property
    def rows(self) -> list[dict]:
        """When selecting a ponente or voting members, create a candidate list of
        justices based on the `valid_date`.

        Returns:
            list[dict]: Filtered list of justices
        """  # noqa: E501
        if not self.valid_date:
            return []
        criteria = "inactive_date > :date and :date > start_term"
        params = {"date": self.valid_date.isoformat()}
        results = self.table.rows_where(
            where=criteria,
            where_args=params,
            select=(
                "id, full_name, lower(last_name) surname, alias, start_term,"
                " inactive_date, chief_date"
            ),
            order_by="start_term desc",
        )
        justice_list = list(results)
        sorted_list = sorted(justice_list, key=lambda d: d["id"])
        return sorted_list

    @property
    def choice(self) -> dict | None:
        """Based on `@rows`, match the cleaned_name to either the alias
        of the justice or the justice's last name; on match, determine whether the
        designation should be 'C.J.' or 'J.'
        """  # noqa: E501
        candidate_options = []
        if not self.valid_date:
            return None

        if self.text:
            # Special rule for duplicate last names
            if "Lopez" in self.text:
                if "jhosep" in self.text.lower():
                    for candidate in self.rows:
                        if int(candidate["id"]) == 190:
                            candidate_options.append(candidate)
                elif "mario" in self.text.lower():
                    for candidate in self.rows:
                        if int(candidate["id"]) == 185:
                            candidate_options.append(candidate)

        # only proceed to add more options if special rule not met
        if not candidate_options:
            if not self.candidate:
                return None

            for candidate in self.rows:
                if candidate["alias"] and candidate["alias"] == self.candidate:
                    candidate_options.append(candidate)
                    continue
                elif candidate["surname"] == self.candidate:
                    candidate_options.append(candidate)
                    continue

        if candidate_options:
            if len(candidate_options) == 1:
                res = candidate_options[0]
                res.pop("alias")
                res["surname"] = res["surname"].title()
                res["designation"] = "J."
                if chief_date := res.get("chief_date"):
                    s = parse(chief_date).date()
                    e = parse(res["inactive_date"]).date()
                    if s < self.valid_date < e:
                        res["designation"] = "C.J."
                return res
            else:
                msg = f"Too many {candidate_options=} for {self.candidate=} on {self.valid_date=}. Consider manual intervention."  # noqa: E501
                logging.error(msg)

        if self.text:
            if matches := get_close_matches(
                self.text,
                possibilities=[row["full_name"] for row in self.rows],
                n=1,
                cutoff=0.7,
            ):
                if options := list(
                    self.db[self.tablename].rows_where(
                        "full_name = ?", where_args=(matches[0],)
                    )
                ):
                    res: dict[str, str] = {}
                    selected = options[0]
                    res["id"] = selected["id"]
                    res["surname"] = selected["last_name"]
                    res["designation"] = "J."
                    if chief_date := selected.get("chief_date"):
                        s = parse(chief_date).date()
                        e = parse(res["inactive_date"]).date()
                        if s < self.valid_date < e:
                            res["designation"] = "C.J."
                    return res

        return None

    @property
    def detail(self) -> JusticeDetail | None:
        """Get object to match fields directly

        Returns:
            JusticeDetail | None: Can subsequently be used in third-party library.
        """  # noqa: E501
        if not self.src:
            return None

        if self.src.per_curiam:
            return JusticeDetail(
                justice_id=None,
                raw_ponente=None,
                designation=None,
                per_curiam=True,
            )
        elif self.choice and self.choice.get("id", None):
            digit_id = int(self.choice["id"])
            return JusticeDetail(
                justice_id=digit_id,
                raw_ponente=self.choice["surname"],
                designation=self.choice["designation"],
                per_curiam=False,
            )
        return None

    @property
    def id(self) -> int | None:
        return self.detail.justice_id if self.detail else None

    @property
    def per_curiam(self) -> bool:
        return self.detail.per_curiam if self.detail else False

    @property
    def raw_ponente(self) -> str | None:
        return self.detail.raw_ponente if self.detail else None

    @property
    def ponencia(self) -> dict[str, Any]:
        """Produces a dict of partial fields that include the following keys:

        1. `justice_id`: int
        2. `raw_ponente`: str
        3. `per_curiam`: bool
        """
        return {
            "justice_id": self.id,
            "raw_ponente": self.raw_ponente,
            "per_curiam": self.per_curiam,
        }

Attributes

choice: dict | None property

Based on @rows, match the cleaned_name to either the alias of the justice or the justice's last name; on match, determine whether the designation should be 'C.J.' or 'J.'

detail: JusticeDetail | None property

Get object to match fields directly

Returns:

Type Description
JusticeDetail | None

JusticeDetail | None: Can subsequently be used in third-party library.

ponencia: dict[str, Any] property

Produces a dict of partial fields that include the following keys:

  1. justice_id: int
  2. raw_ponente: str
  3. per_curiam: bool

rows: list[dict] property

When selecting a ponente or voting members, create a candidate list of justices based on the valid_date.

Returns:

Type Description
list[dict]

list[dict]: Filtered list of justices

View chief justice dates

SQL
WITH end_chief_date(d) AS (
    -- For each chief justice, get a second date: the date that the next chief justice is appointed; Can get this by getting the first chief date greater than the present chief date and using an ascending order. */
    SELECT
        DATE(
            tbl2.chief_date,
            '-1 day'
        )
    FROM
        {{ justice_table }}
        tbl2
    WHERE
        tbl2.chief_date IS NOT NULL
        AND tbl2.chief_date > tbl1.chief_date
    ORDER BY
        tbl2.chief_date ASC
    LIMIT
        1
), time_as_chief(period) AS (
    -- Difference between the two chief dates: that will be the time served as chief in years format */
    SELECT
        (
            SELECT
                DATE(d)
            FROM
                end_chief_date
        ) - DATE(
            tbl1.chief_date
        )
)
SELECT
    tbl1.id,
    tbl1.last_name,
    tbl1.chief_date,
    (
        SELECT
            d
        FROM
            end_chief_date
    ) max_end_chief_date,
    MIN(
        tbl1.inactive_date,
        (
            SELECT
                d
            FROM
                end_chief_date
        )
    ) actual_inactive_as_chief,
    (
        SELECT
            period
        FROM
            time_as_chief
    ) years_as_chief
FROM
    {{ justice_table }}
    tbl1
WHERE
    tbl1.chief_date IS NOT NULL
ORDER BY
    tbl1.chief_date DESC
Python
[
    {
        'id': 178,
        'last_name': 'Gesmundo',
        'chief_date': '2021-04-05',
        'max_end_chief_date': None,
        'actual_inactive_as_chief': None,
        'years_as_chief': None
    },
    {
        'id': 162,
        'last_name': 'Peralta',
        'chief_date': '2019-10-23',
        'max_end_chief_date': '2021-04-04',
        'actual_inactive_as_chief': '2021-03-27',
        'years_as_chief': 2
    },
    {
        'id': 163,
        'last_name': 'Bersamin',
        'chief_date': '2018-11-26',
        'max_end_chief_date': '2019-10-22',
        'actual_inactive_as_chief': '2019-10-18',
        'years_as_chief': 1
    },
    {
        'id': 160,
        'last_name': 'Leonardo-De Castro',
        'chief_date': '2018-08-28',
        'max_end_chief_date': '2018-11-25',
        'actual_inactive_as_chief': '2018-10-08',
        'years_as_chief': 0
    }...
]