""" Citation formatters: Vancouver, APA 7th edition, and AMA style. Vancouver is the standard for UK nursing and medical publications. APA 7th is used by some UK university nursing programmes. """ # --------------------------------------------------------------------------- # APA 7th Edition # --------------------------------------------------------------------------- def format_apa7(article: dict) -> str: """ APA 7th edition journal article format: Author, F. M., & Author, F. M. (Year). Title of article. *Journal Name*, *Volume*(Issue), Pages. https://doi.org/xxxxx """ authors_str = _apa_authors(article.get("authors", [])) year = article.get("year") or "n.d." title = _clean_title(article.get("title", "")) journal = article.get("journal", "") volume = article.get("volume", "") issue = article.get("issue", "") pages = article.get("pages", "") doi = _clean_doi(article.get("doi", "")) url = article.get("url", "") citation = f"{authors_str} ({year}). {title}." if journal: citation += f" *{journal}*" if volume: citation += f", *{volume}*" if issue: citation += f"({issue})" if pages: citation += f", {pages}" citation += "." if doi: citation += f" https://doi.org/{doi}" elif url: citation += f" {url}" return citation def _apa_authors(authors: list[str]) -> str: if not authors: return "Unknown Author" formatted = [_apa_single(a) for a in authors[:20]] # APA 7: 21+ authors → first 19, ellipsis, last author if len(authors) > 20: last = _apa_single(authors[-1]) return ", ".join(formatted[:19]) + ", . . . " + last if len(formatted) == 1: return formatted[0] if len(formatted) == 2: return f"{formatted[0]}, & {formatted[1]}" return ", ".join(formatted[:-1]) + f", & {formatted[-1]}" def _apa_single(name: str) -> str: name = name.strip() if name.lower() in ("et al.", "et al"): return "et al." if "," in name: # "Last, First M." → keep as-is, just ensure initials last, *rest = name.split(",", 1) initials = _initials(" ".join(rest)) return f"{last.strip()}, {initials}" parts = name.split() if len(parts) >= 2: # Detect PubMed/Europe PMC "Surname JK" format if _is_initials_token(parts[-1]): last = " ".join(parts[:-1]) # handles "van der Berg JK" initials = _initials_from_str(parts[-1]) return f"{last}, {initials}" last = parts[-1] initials = _initials(" ".join(parts[:-1])) return f"{last}, {initials}" return name # --------------------------------------------------------------------------- # Vancouver Style (UK nursing / medical standard) # --------------------------------------------------------------------------- def format_vancouver(article: dict) -> str: """ Vancouver citation format — standard for UK nursing and medical journals (Nursing Standard, BJN, JAN, BMJ, Lancet, NEJM etc.) Format: Surname Initial(s), Surname Initial(s). Title. Journal Abbrev. Year;Volume(Issue):Pages. doi:xxxxx Up to 6 authors; if more, list first 6 then 'et al.' """ authors_str = _vancouver_authors(article.get("authors", [])) year = article.get("year", "") title = _clean_title(article.get("title", "")) journal = article.get("journal", "") volume = article.get("volume", "") issue = article.get("issue", "") pages = article.get("pages", "") doi = _clean_doi(article.get("doi", "")) url = article.get("url", "") # Vancouver: no period after title if journal follows immediately citation = f"{authors_str}. {title}." if journal: citation += f" {journal}." if year: citation += f" {year}" if volume: citation += f";{volume}" if issue: citation += f"({issue})" if pages: citation += f":{pages}" citation += "." if doi: citation += f" doi:{doi}" elif url: citation += f" Available from: {url}" return citation def _vancouver_authors(authors: list[str]) -> str: if not authors: return "Unknown Author" formatted = [_vancouver_single(a) for a in authors[:6]] if len(authors) > 6: formatted.append("et al") return ", ".join(formatted) def _vancouver_single(name: str) -> str: """Convert name to Vancouver format: Surname AB""" name = name.strip() if name.lower() in ("et al.", "et al"): return "et al" if "," in name: last, *rest = name.split(",", 1) initials = "".join(p[0].upper() for p in " ".join(rest).split() if p) return f"{last.strip()} {initials}" parts = name.split() if len(parts) >= 2: # Detect PubMed/Europe PMC "Surname JK" format if _is_initials_token(parts[-1]): last = " ".join(parts[:-1]) initials = parts[-1].upper() # already run-together: "JK" stays "JK" return f"{last} {initials}" last = parts[-1] initials = "".join(p[0].upper() for p in parts[:-1] if p) return f"{last} {initials}" return name # --------------------------------------------------------------------------- # Harvard Style (Cite Them Right — common UK university nursing standard) # --------------------------------------------------------------------------- def format_harvard(article: dict) -> str: """ Harvard citation format (Cite Them Right 12th ed.) — used by many UK universities for nursing essays where Vancouver is not mandated. Format: Surname, Initial(s). and Surname, Initial(s). (Year) 'Title of article', *Journal Name*, Volume(Issue), pp. Pages. Up to 3 authors listed; 4+ → first author et al. """ authors_str = _harvard_authors(article.get("authors", [])) year = article.get("year") or "n.d." title = _clean_title(article.get("title", "")) journal = article.get("journal", "") volume = article.get("volume", "") issue = article.get("issue", "") pages = article.get("pages", "") doi = _clean_doi(article.get("doi", "")) url = article.get("url", "") citation = f"{authors_str} ({year}) '{title}'," if journal: citation += f" *{journal}*," if volume: citation += f" {volume}" if issue: citation += f"({issue})" citation += "," if pages: citation += f" pp. {pages}." else: citation = citation.rstrip(",") + "." else: citation = citation.rstrip(",") + "." if doi: citation += f" doi:{doi}" elif url: citation += f" Available at: {url} (Accessed: {_access_year(year)})" return citation def _harvard_authors(authors: list[str]) -> str: if not authors: return "Anon." if len(authors) > 3: return _harvard_single(authors[0]) + " et al." formatted = [_harvard_single(a) for a in authors] if len(formatted) == 1: return formatted[0] return ", ".join(formatted[:-1]) + " and " + formatted[-1] def _harvard_single(name: str) -> str: """Convert to Harvard format: Surname, A.B.""" name = name.strip() if name.lower() in ("et al.", "et al"): return "et al." if "," in name: last, *rest = name.split(",", 1) initials = _initials(" ".join(rest)) return f"{last.strip()}, {initials}" parts = name.split() if len(parts) >= 2: # Detect PubMed/Europe PMC "Surname JK" format (e.g. "Smith JK") # Without this check, "Smith JK" → last="JK", initials="S." → "JK, S." (wrong) if _is_initials_token(parts[-1]): last = " ".join(parts[:-1]) # handles multi-word surnames too initials = _initials_from_str(parts[-1]) # "JK" → "J. K." return f"{last}, {initials}" last = parts[-1] initials = _initials(" ".join(parts[:-1])) return f"{last}, {initials}" return name def _access_year(year: str) -> str: """Return a sensible access date placeholder.""" return "date accessed" # --------------------------------------------------------------------------- # AMA Style # --------------------------------------------------------------------------- def format_ama(article: dict) -> str: """ AMA citation format (used in some nursing / medical journals): Last FM, Last FM. Title. Journal. Year;Vol(Issue):Pages. doi:xxxxx """ authors_str = _ama_authors(article.get("authors", [])) year = article.get("year", "") title = _clean_title(article.get("title", "")) journal = article.get("journal", "") volume = article.get("volume", "") issue = article.get("issue", "") pages = article.get("pages", "") doi = _clean_doi(article.get("doi", "")) citation = f"{authors_str}. {title}. *{journal}*." if year: citation += f" {year}" if volume: citation += f";{volume}" if issue: citation += f"({issue})" if pages: citation += f":{pages}" citation += "." if doi: citation += f" doi:{doi}" return citation def _ama_authors(authors: list[str]) -> str: if not authors: return "Unknown Author" formatted = [_ama_single(a) for a in authors[:6]] if len(authors) > 6: formatted.append("et al") return ", ".join(formatted) def _ama_single(name: str) -> str: name = name.strip() if name.lower() in ("et al.", "et al", "et al."): return "et al" if "," in name: last, *rest = name.split(",", 1) initials = "".join(p[0].upper() for p in " ".join(rest).split() if p) return f"{last.strip()} {initials}" parts = name.split() if len(parts) >= 2: # Detect PubMed/Europe PMC "Surname JK" format if _is_initials_token(parts[-1]): last = " ".join(parts[:-1]) initials = parts[-1].upper() # AMA uses run-together: "Smith JK" return f"{last} {initials}" last = parts[-1] initials = "".join(p[0].upper() for p in parts[:-1] if p) return f"{last} {initials}" return name # --------------------------------------------------------------------------- # Shared helpers # --------------------------------------------------------------------------- def _initials(first_middle: str) -> str: """Convert first/middle name string to initials: "John Paul" → "J. P." """ parts = first_middle.strip().split() return " ".join(p[0].upper() + "." for p in parts if p) def _initials_from_str(token: str) -> str: """ Expand a run-together initials token from PubMed/Europe PMC into dotted initials: "JK" → "J. K." | "AB" → "A. B." | "J" → "J." """ return " ".join(c.upper() + "." for c in token if c.isalpha()) def _is_initials_token(token: str) -> bool: """ Return True when a name token looks like run-together initials rather than a proper word — i.e. it is 1–4 uppercase letters with no vowels OR is entirely uppercase and short. Examples that return True : "JK", "AB", "J", "JKL", "ABCD" Examples that return False: "Smith", "John", "van", "De", "O'Brien" """ if not token or not token.isalpha(): return False if len(token) > 4: return False # Single letter is always an initial if len(token) == 1: return token.isupper() # Multi-char: must be all-uppercase (PubMed stores "Smith JK", not "Smith jk") return token.isupper() def _clean_title(title: str) -> str: return title.strip().rstrip(".") def _clean_doi(doi: str) -> str: doi = doi.strip() for prefix in ("https://doi.org/", "http://doi.org/", "doi:", "doi: "): if doi.lower().startswith(prefix): doi = doi[len(prefix):] return doi