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
4 changes: 2 additions & 2 deletions bib_lookup/styles/apa.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ def get_article_template(self, e: Entry) -> Node:
optional[
join(sep="")[
", ",
tag("em")[optional_field("volume")],
tag("em")[field("volume")],
]
],
optional[
Expand All @@ -114,7 +114,7 @@ def get_article_template(self, e: Entry) -> Node:
")",
]
],
optional[join(sep="", last_sep="")[", ", optional_field("pages")]],
optional[join(sep="", last_sep="")[", ", field("pages")]],
".",
]
if "doi" in e.fields:
Expand Down
123 changes: 75 additions & 48 deletions bib_lookup/styles/chicago.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,42 @@
words,
)

_CHICAGO_MONTH_MAP: dict[str, str] = {
"1": "January",
"01": "January",
"2": "February",
"02": "February",
"3": "March",
"03": "March",
"4": "April",
"04": "April",
"5": "May",
"05": "May",
"6": "June",
"06": "June",
"7": "July",
"07": "July",
"8": "August",
"08": "August",
"9": "September",
"09": "September",
"10": "October",
"11": "November",
"12": "December",
"january": "January",
"february": "February",
"march": "March",
"april": "April",
"may": "May",
"june": "June",
"july": "July",
"august": "August",
"september": "September",
"october": "October",
"november": "November",
"december": "December",
}


class ChicagoNames(Node):
def __init__(self, role: str, formatter: Callable[[Person, bool], str], limit: int = 10):
Expand Down Expand Up @@ -84,49 +120,30 @@ def chicago_date(children: Node, data: Union[dict, Entry]) -> str:
month = data.fields.get("month", "")
year = data.fields.get("year", "")
if month and year:
# Full month name for Chicago
MONTH_MAP: dict[str, str] = {
"1": "January",
"01": "January",
"2": "February",
"02": "February",
"3": "March",
"03": "March",
"4": "April",
"04": "April",
"5": "May",
"05": "May",
"6": "June",
"06": "June",
"7": "July",
"07": "July",
"8": "August",
"08": "August",
"9": "September",
"09": "September",
"10": "October",
"11": "November",
"12": "December",
"january": "January",
"february": "February",
"march": "March",
"april": "April",
"may": "May",
"june": "June",
"july": "July",
"august": "August",
"september": "September",
"october": "October",
"november": "November",
"december": "December",
}
month_full = MONTH_MAP.get(str(month).lower(), str(month).capitalize())
month_full = _CHICAGO_MONTH_MAP.get(str(month).lower(), str(month).capitalize())
return f"({month_full} {year})"
elif year:
return f"({year})"
return ""


@node
def chicago_date_plain(children: Node, data: Union[dict, Entry]) -> str:
"""Like chicago_date but without surrounding parentheses."""
if isinstance(data, dict) and "entry" in data:
data = data["entry"]
if not hasattr(data, "fields"):
return ""
month = data.fields.get("month", "")
year = data.fields.get("year", "")
if month and year:
month_full = _CHICAGO_MONTH_MAP.get(str(month).lower(), str(month).capitalize())
return f"{month_full} {year}"
elif year:
return year
return ""


class ChicagoStyle(UnsrtStyle):
def __init__(self, max_names: int = 10, **kwargs: Any):
super().__init__(**kwargs)
Expand Down Expand Up @@ -154,22 +171,32 @@ def format_label(self, entry: Entry) -> str:
return ""

def get_article_template(self, e: Entry) -> Node:
# Author. "Title." Journal Volume (Month Year): Pages. https://doi.org/...
# Author. "Title." Journal ...
template = join(sep=". ")[
self.format_names("author", as_sentence=False),
join(sep="")["“", field("title"), ".”"],
]

journal_info = join(sep=" ")[
tag("em")[field("journal")],
join(sep="")[
optional_field("volume"),
optional[join(sep="")[", no. ", field("number")]],
" ",
chicago_date,
optional[join(sep="", last_sep="")[": ", chicago_pages]],
],
]
if e.fields.get("volume"):
# With volume: Journal Vol, no. N (Month Year): Pages
journal_info = join(sep=" ")[
tag("em")[field("journal")],
join(sep="")[
optional_field("volume"),
optional[join(sep="")[", no. ", field("number")]],
" ",
chicago_date,
optional[join(sep="", last_sep="")[": ", chicago_pages]],
],
]
else:
# Without volume: Journal, Month Year, Pages (comma-separated, no parens)
journal_info = join(sep=", ")[
tag("em")[field("journal")],
chicago_date_plain,
chicago_pages,
]
Comment on lines +194 to +198
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve issue number when Chicago volume is absent

In ChicagoStyle.get_article_template, the no-volume branch no longer references field("number"), so entries that have an issue number but no volume now silently lose that bibliographic detail. This regression is introduced by the new if e.fields.get("volume") split: previously the template still emitted , no. N when number existed, but the new branch outputs only journal/date/pages, which can make citations ambiguous for issue-only journals.

Useful? React with 👍 / 👎.


template = join(sep=" ")[template, journal_info]
# Add ISSN if present
if "issn" in e.fields:
Expand Down
40 changes: 34 additions & 6 deletions bib_lookup/styles/gbt7714.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
Node,
field,
join,
optional,
node,
optional_field,
sentence,
)
Expand Down Expand Up @@ -60,6 +60,38 @@ def format_data(self, data: Union[dict, Entry]) -> str:
return result


@node
def gbt_year_vol_pages(children: Node, data: Union[dict, Entry]) -> str:
"""Format year, volume, number and pages per GB/T 7714-2015.

- With volume: ``year, vol(num): pages``
- Without volume: ``year: pages``

Page ranges are normalized to use a regular hyphen (not en-dash).
"""
if isinstance(data, dict) and "entry" in data:
data = data["entry"]
if not hasattr(data, "fields"):
return ""
year = data.fields.get("year", "")
volume = data.fields.get("volume", "")
number = data.fields.get("number", "")
# Normalize page-range separators: en-dash / double-hyphen → single hyphen
pages = data.fields.get("pages", "").replace("\u2013", "-").replace("--", "-")

vol_str = ""
if volume:
vol_str = f"{volume}({number})" if number else volume
Comment on lines +83 to +84
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve issue number in GBT output without volume

gbt_year_vol_pages now ignores number unless volume is present (vol_str is only built inside if volume:), so issue-only article metadata is dropped from formatted output. This is a behavior change from the previous template, which still rendered number in the article suffix, and it reduces citation accuracy for journals that provide issue but not volume.

Useful? React with 👍 / 👎.


if vol_str and pages:
return f"{year}, {vol_str}: {pages}"
if vol_str:
return f"{year}, {vol_str}"
if pages:
return f"{year}: {pages}"
Comment on lines +87 to +91
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gbt_year_vol_pages can emit leading punctuation when year is missing (e.g., returns ", {vol_str}: ..." or ", {vol_str}") because the comma is always included even if year is empty. Consider either treating year as required (raise FieldIsMissing / use field('year')) or only adding the comma when year is non-empty.

Suggested change
return f"{year}, {vol_str}: {pages}"
if vol_str:
return f"{year}, {vol_str}"
if pages:
return f"{year}: {pages}"
if year:
return f"{year}, {vol_str}: {pages}"
return f"{vol_str}: {pages}"
if vol_str:
if year:
return f"{year}, {vol_str}"
return vol_str
if pages:
if year:
return f"{year}: {pages}"
return pages

Copilot uses AI. Check for mistakes.
Comment on lines +86 to +91
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When pages is present but year is missing, this returns ": {pages}", which will render malformed output (leading colon). Suggest returning just {pages} in that case, or ensuring year is required before adding the : pages suffix.

Suggested change
if vol_str and pages:
return f"{year}, {vol_str}: {pages}"
if vol_str:
return f"{year}, {vol_str}"
if pages:
return f"{year}: {pages}"
# Preserve original formatting when year is present, but avoid
# leading commas/colons when year is missing.
if vol_str and pages and year:
return f"{year}, {vol_str}: {pages}"
if vol_str and year:
return f"{year}, {vol_str}"
if pages and year:
return f"{year}: {pages}"
if vol_str and pages:
return f"{vol_str}: {pages}"
if vol_str:
return vol_str
if pages:
return pages

Copilot uses AI. Check for mistakes.
return year


class GBT7714Style(UnsrtStyle):
def __init__(
self,
Expand Down Expand Up @@ -108,11 +140,7 @@ def get_article_template(self, e: Entry) -> Node:
join[field("title"), medium_tag],
join(sep=", ")[
field("journal"),
field("year"),
join(sep=": ")[
join[optional_field("volume"), optional["(", field("number"), ")"]],
optional_field("pages"),
],
gbt_year_vol_pages,
],
]
if "url" in e.fields:
Expand Down
7 changes: 4 additions & 3 deletions bib_lookup/styles/ieee.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,10 @@ def ieee_pages(children: Node, data: Union[dict, Entry]) -> str:
pages = data.fields.get("pages", "")
if not pages:
return ""
# If pages contains range (- or --)
if "-" in pages or "," in pages:
return f"pp. {pages.replace('--', '–')}"
# If pages contains range (-, -- or en-dash –)
if "-" in pages or "\u2013" in pages or "," in pages:
en_dash = "\u2013"
return f"pp. {pages.replace('--', en_dash)}"
else:
# For page numbers >= 100000 (6+ digits), add space between groups of three digits
# e.g., 115006 -> 115 006, but 103834 stays as 103834 (per expected output)
Expand Down
50 changes: 48 additions & 2 deletions test/test_styles.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
from pybtex.style.template import Node

from bib_lookup.styles.apa import APANames, APAStyle
from bib_lookup.styles.chicago import ChicagoNames, ChicagoStyle, chicago_date, chicago_pages
from bib_lookup.styles.gbt7714 import GBT7714Style, GBTNames
from bib_lookup.styles.chicago import ChicagoNames, ChicagoStyle, chicago_date, chicago_date_plain, chicago_pages
from bib_lookup.styles.gbt7714 import GBT7714Style, GBTNames, gbt_year_vol_pages
from bib_lookup.styles.ieee import IEEENames, IEEEStyle, ieee_month, ieee_pages

EXAMPLES = [
Expand Down Expand Up @@ -110,6 +110,25 @@
"gbt7714": """[1] WEN H, KANG J. A Novel Deep Learning Package for Electrocardiography Research[J/OL]. Physiological Measurement, 2022, 43(11): 115006. https://doi.org/10.1088/1361-6579/ac9451. DOI: 10.1088/1361-6579/ac9451.""",
"chicago": """Wen, Hao, and Jingsu Kang. “A Novel Deep Learning Package for Electrocardiography Research.” Physiological Measurement 43, no. 11 (November 2022): 115006. ISSN: 1361-6579. https://doi.org/10.1088/1361-6579/ac9451. https://doi.org/10.1088/1361-6579/ac9451.""",
},
# no volume / no number (page-range with en-dash)
{
"entry": """
@article{Groot_Bruinderink_2018,
title = {{Differential Fault Attacks on Deterministic Lattice Signatures}},
author = {Groot Bruinderink, Leon and Pessl, Peter},
journal = {{IACR Transactions on Cryptographic Hardware and Embedded Systems}},
issn = {2569-2925},
doi = {10.46586/tches.v2018.i3.21-43},
publisher = {{Universitatsbibliothek der Ruhr-Universitat Bochum}},
year = {2018},
month = {8},
pages = {21–43}
}""",
"apa": """Groot Bruinderink, L., & Pessl, P. (2018). Differential Fault Attacks on Deterministic Lattice Signatures. IACR Transactions on Cryptographic Hardware and Embedded Systems, 21–43. https://doi.org/10.46586/tches.v2018.i3.21-43""",
"ieee": """[1] L. Groot Bruinderink and P. Pessl, “Differential Fault Attacks on Deterministic Lattice Signatures,” IACR Transactions on Cryptographic Hardware and Embedded Systems, pp. 21–43, Aug. 2018, ISSN: 2569-2925. DOI: 10.46586/tches.v2018.i3.21-43.""",
"gbt7714": """[1] GROOT BRUINDERINK L, PESSL P. Differential Fault Attacks on Deterministic Lattice Signatures[J/OL]. IACR Transactions on Cryptographic Hardware and Embedded Systems, 2018: 21-43. DOI: 10.46586/tches.v2018.i3.21-43.""",
"chicago": """Groot Bruinderink, Leon, and Peter Pessl. “Differential Fault Attacks on Deterministic Lattice Signatures.” IACR Transactions on Cryptographic Hardware and Embedded Systems, August 2018, 21–43. ISSN: 2569-2925. https://doi.org/10.46586/tches.v2018.i3.21-43.""",
},
]


Expand Down Expand Up @@ -493,6 +512,33 @@ def ctx(entry):
# Test IEEE format_label
assert style_ieee.format_label(Entry("a")) == ""

# Test chicago_date_plain with month + year
entry_month_year = Entry("a", fields={"month": "8", "year": "2018"})
assert _r(chicago_date_plain, entry_month_year) == "August 2018"

# Test chicago_date_plain with year only
entry_year_only2 = Entry("a", fields={"year": "2021"})
assert _r(chicago_date_plain, entry_year_only2) == "2021"

# Test chicago_date_plain with no fields
assert _r(chicago_date_plain, Entry("a")) == ""

# Test gbt_year_vol_pages: no volume, with pages (uses colon separator, hyphen in range)
entry_no_vol = Entry("a", fields={"year": "2018", "pages": "21\u201343"})
assert _r(gbt_year_vol_pages, entry_no_vol) == "2018: 21-43"

# Test gbt_year_vol_pages: with volume and number
entry_vol_num = Entry("a", fields={"year": "2022", "volume": "43", "number": "11", "pages": "115006"})
assert _r(gbt_year_vol_pages, entry_vol_num) == "2022, 43(11): 115006"

# Test gbt_year_vol_pages: with volume only (no number, no pages)
entry_vol_only = Entry("a", fields={"year": "2021", "volume": "120"})
assert _r(gbt_year_vol_pages, entry_vol_only) == "2021, 120"

# Test gbt_year_vol_pages: year only (no volume, no pages)
entry_year_only3 = Entry("a", fields={"year": "2021"})
assert _r(gbt_year_vol_pages, entry_year_only3) == "2021"


def test_misc():
with pytest.raises(ValueError):
Expand Down
Loading