diff --git a/bib_lookup/styles/apa.py b/bib_lookup/styles/apa.py index b569bd7..8009c9c 100644 --- a/bib_lookup/styles/apa.py +++ b/bib_lookup/styles/apa.py @@ -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[ @@ -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: diff --git a/bib_lookup/styles/chicago.py b/bib_lookup/styles/chicago.py index 90e814a..b460163 100644 --- a/bib_lookup/styles/chicago.py +++ b/bib_lookup/styles/chicago.py @@ -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): @@ -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) @@ -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, + ] + template = join(sep=" ")[template, journal_info] # Add ISSN if present if "issn" in e.fields: diff --git a/bib_lookup/styles/gbt7714.py b/bib_lookup/styles/gbt7714.py index c11b3aa..75b0e6e 100644 --- a/bib_lookup/styles/gbt7714.py +++ b/bib_lookup/styles/gbt7714.py @@ -11,7 +11,7 @@ Node, field, join, - optional, + node, optional_field, sentence, ) @@ -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 + + 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}" + return year + + class GBT7714Style(UnsrtStyle): def __init__( self, @@ -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: diff --git a/bib_lookup/styles/ieee.py b/bib_lookup/styles/ieee.py index 810e726..2c470e4 100644 --- a/bib_lookup/styles/ieee.py +++ b/bib_lookup/styles/ieee.py @@ -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) diff --git a/test/test_styles.py b/test/test_styles.py index 98f7edf..9eef306 100644 --- a/test/test_styles.py +++ b/test/test_styles.py @@ -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 = [ @@ -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.""", + }, ] @@ -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):