Add a verbatim pub_date field for dates the parsed fields can't hold - #17
Open
gaurav wants to merge 3 commits into
Open
Add a verbatim pub_date field for dates the parsed fields can't hold#17gaurav wants to merge 3 commits into
pub_date field for dates the parsed fields can't hold#17gaurav wants to merge 3 commits into
Conversation
`pub_year`/`pub_month`/`pub_day` are parsed conveniences, and no arrangement of them represents a cross-year `MedlineDate`. PMID:10188493 is `<MedlineDate>1998 Dec-1999 Jan</MedlineDate>`, which we split into `pub_year: "1998"`, `pub_month: "Dec-1999 Jan"` -- lossless (the two concatenate back) but a field named `pub_month` holding a year is a wart, and every alternative split just moves the ambiguity to a different field (issue #14). NCBI already solved this: `esummary` ships a verbatim `pubdate` string on every record, alongside the parsed parts. Do the same. `pub_date` takes the `MedlineDate` whole when present, else assembles year + normalized month + day, and is populated for every record so consumers never branch. It fixes every lossy shape at once -- seasons, bare year ranges, "n.d." -- not just the one that prompted it. The two renderings PubMed serves must converge on one string: efetch's `<Year>1994</Year><Season>Sep-Dec</Season>` and the baseline's `<MedlineDate>1994 Sep-Dec</MedlineDate>` are the same record, and both now give "1994 Sep-Dec". That convergence is what lets `validate` compare the field at all, so it is asserted directly rather than left implied. `_PUB_DATE_SQL` calls `_normalize_month_sql('la.pub_month')` and *not* `_PUB_MONTH_SQL`: the latter already folds the `MedlineDate` back in, which would double-count it on the branch that only runs when there isn't one. Also records why `pub_year` takes the *leading* year of a range. The trailing year is arguably the better semantic answer -- "1998 Dec-1999 Jan" mostly reached readers in Jan 1999 -- but NCBI's own `sortpubdate` uses the leading year on every cross-year shape, every other shape already does, and cross-year ranges are ~0.07% of PubMed (4 of 5,773 sampled records, three of them bare "1987-1988"). The docstring carries the examples, the counterargument and the evidence, since that is where a reader will look. Verified end-to-end: PMIDs 30690000, 8000234 and 10188493 through parse -> load -> export produce `pub_date` byte-identical to NCBI esummary's `pubdate`, and 8000234's two renderings converge. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Records the split the new field creates: `pub_date` is the fidelity guarantee, the three parsed fields are conveniences, and consumers pick by what they are doing (rendering vs. sorting) rather than by record shape. Also notes the `_PUB_MONTH_SQL` trap in `_PUB_DATE_SQL`, and that the field count locked by `test_expected_fields_matches_spec` is now twelve. FUTURE.md gains the other half of NCBI's design as a deliberate deferral: `sortpubdate` is a normalized sort key we did not take, because nothing downstream range-filters yet. Worth knowing NCBI's own answer for an unparseable range is to drop precision (1998/01/01) rather than guess. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The `month-format` check's accepted set was widened from the 12 abbreviations to months, seasons and ranges of those, but nothing tested it. A too-narrow set would warn on every one of the ~7% of records carrying a season or range; a wide-open one would stop flagging anything. The parametrized test pins both edges, including that "Dec-1999 Jan" is deliberately *still* a warning. Not tested: that the set no longer depends on `LC_TIME`. Exercising it means `locale.setlocale`, which needs the locale installed on the machine running the suite -- flaky across environments for a guarantee the import already makes structurally. CLAUDE.md gains two things this session needed and had to work out from scratch: that `esummary` is a third rendering of a record, and the one that shows NCBI's own normalization decisions (which is what settled `pub_date`'s shape and the leading-year rule); and that "how common is this shape in PubMed?" is answerable in under a minute by sampling random PMIDs through `esummary`, rather than by downloading a 30 MB baseline file that answers for one file only. Both drove design calls here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #15 — review that first; this PR's base is
support-approximate-dates.Addresses the cross-year case raised on #14.
The problem
pub_year/pub_month/pub_dayare parsed conveniences, and no arrangement of three fields represents a cross-year range. PMID:10188493 is<MedlineDate>1998 Dec-1999 Jan</MedlineDate>, which we split into:Lossless — the two concatenate back to the source — but a field named
pub_monthholding a year is a wart, and every alternative split just moves the ambiguity somewhere else.NCBI already solved this
esummaryships a verbatimpubdateon every record, alongside a normalizedsortpubdate:pubdatesortpubdate"2019 Mar 15"2019/03/15"1994 Sep-Dec"1994/09/01"1998 Dec-1999 Jan"1998/01/01This PR takes the first half.
pub_datecarries PubMed's own string verbatim, on every record. The three parsed fields stay exactly as they are. Consumers rendering a citation readpub_date; consumers sorting or filtering readpub_year; neither parses the other's output.It fixes every lossy shape at once — seasons, bare year ranges,
"n.d."— not just the one that prompted it.The rule
The convergence is the load-bearing property. PubMed serves the same record as
<Year>+<Season>from efetch and as a bare<MedlineDate>in the baseline; both must produce one string, orvalidatereads every such record as a mismatch. That's asserted directly rather than left implied.Why
pub_yearkeeps the leading yearLeft unchanged at
1998, not1999, and the reasoning is now written into_year_from_medline_date's docstring where a reader will look. The trailing year is arguably the better semantic answer — the issue mostly reached readers in Jan 1999 — but:sortpubdateuses the leading year on every cross-year shape sampled ("1997 Dec-1998 Jan"→ 1997,"1987-1988"→ 1987). A consumer joining ourpub_yearagainst anything Entrez-derived would disagree otherwise."1987-1988"ranges we already handle. The problem shape proper is ~1 in 5,773 (~7k records of 40.9M).Anyone who needs
1999can read it frompub_date.Verification
test_pub_date_sql_matches_python— 4-way cross product of year × month × day × MedlineDate (~1,080 cases), the same twin-pinning pattern aspub_month. The 4-way product matters: a disagreement can hide in any pairing, notably an absent month leaving a double space in one implementation but not the other.test_pub_date_matches_ncbi_pubdate— offline table pinned to esummary's real output for the PMIDs above, including both renderings of 8000234.parse → load → export, then diffed against live esummary —pub_datebyte-identical for all three, and 8000234's two renderings converge.validatenow comparespub_dateas a core field against Entrez.Notes
medline_dateand the three components are already stored; this is entirely export-layer. A re-export is required to ship it.test_expected_fields_matches_spec. Nine are DocumentMetadataAPI's;id,identifiersand nowpub_dateare ours. Worth raising on Replace DocumentMetadataAPI by incorporating it into Node Annotator NCATSTranslator/Core-Components-Working-Group#15, where the field set is already under discussion (URLs, authors).pub_monthranges #16 — with the raw string preserved, normalizing"September-December"→"Sep-Dec"insidepub_monthbecomes cosmetic.sortpubdate, the other half of NCBI's design. Nothing downstream range-filters yet; recorded in FUTURE.md.TODO
Before merge:
<Season>prevalence in the archival XML.zgrep -c "<Season>" <a baseline file>. Every case seen so far is<MedlineDate>in the baseline with<Season>appearing only from efetch, so the<Season>half of Keep approximate months inpub_month(issue #14) #15 may be pure insurance. FUTURE.md currently records this as unmeasured — the answer either confirms that claim or lets it be deleted.pub_dateorpubdate. Raised on CCWG#15.pub_dateis consistent with the neighbouringpub_year/pub_month/pub_day;pubdatematches NCBI exactly. One line in_JSON_FIELDSwhile this PR is open, a re-export after it lands.After merge / separate:
pub_monthranges #16's query against a full load. With the raw string now preserved inpub_date, normalizing"September-December"→"Sep-Dec"insidepub_monthis cosmetic rather than a fidelity question; expect this to close as won't-fix unless the counts surprise. Needs a full corpus load.sortpubdateequivalent. The other half of NCBI's design — a normalizedYYYY/MM/DDsort key — deliberately not taken here, since nothing downstream range-filters yet. Recorded in FUTURE.md. Needs a consumer that wants ordering, plus its own call on what to emit for an unparseable range (NCBI drops precision to January rather than guessing).🤖 Generated with Claude Code