Skip to content

Commit 5169d67

Browse files
Omikhleiaalerque
authored andcommitted
feat(packages): Support biblatex date field and improve date formatting
1 parent 696413f commit 5169d67

File tree

3 files changed

+202
-3
lines changed

3 files changed

+202
-3
lines changed

packages/bibtex/init.lua

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package._name = "bibtex"
66
local epnf = require("epnf")
77
local nbibtex = require("packages.bibtex.support.nbibtex")
88
local namesplit, parse_name = nbibtex.namesplit, nbibtex.parse_name
9+
local isodatetime = require("packages.bibtex.support.isodatetime")
910

1011
local Bibliography
1112

@@ -85,6 +86,8 @@ end)
8586

8687
local bibcompat = require("packages.bibtex.support.bibmaps")
8788
local crossrefmap, fieldmap = bibcompat.crossrefmap, bibcompat.fieldmap
89+
local months =
90+
{ jan = 1, feb = 2, mar = 3, apr = 4, may = 5, jun = 6, jul = 7, aug = 8, sep = 9, oct = 10, nov = 11, dec = 12 }
8891

8992
local function consolidateEntry (entry, label)
9093
local consolidated = {}
@@ -114,6 +117,28 @@ local function consolidateEntry (entry, label)
114117
consolidated[field] = names
115118
end
116119
end
120+
-- Month field in either number or string (3-letter code)
121+
if consolidated.month then
122+
local month = tonumber(consolidated.month) or months[consolidated.month:lower()]
123+
if month and (month >= 1 and month <= 12) then
124+
consolidated.month = month
125+
else
126+
SU.warn("Unrecognized month skipped in entry '" .. label .. "'")
127+
consolidated.month = nil
128+
end
129+
end
130+
-- Extended date fields
131+
for _, field in ipairs({ "date", "origdate", "eventdate", "urldate" }) do
132+
if consolidated[field] then
133+
local dt = isodatetime(consolidated[field])
134+
if dt then
135+
consolidated[field] = dt
136+
else
137+
SU.warn("Invalid '" .. field .. "' skipped in entry '" .. label .. "'")
138+
consolidated[field] = nil
139+
end
140+
end
141+
end
117142
entry.attributes = consolidated
118143
return entry
119144
end
@@ -390,6 +415,10 @@ This text is ignored
390415
}
391416
\end{raw}
392417
418+
Some fields have a special syntax.
419+
The \code{author}, \code{editor} and \code{translator} fields accept a list of names, separated by the keyword \code{and}.
420+
The legacy \code{month} field accepts a three-letter abbreviation for the month in English, or a number from 1 to 12.
421+
The more powerful \code{date} field accepts a date-time following the ISO 8601-2 Extended Date/Time Format specification level 1 (such as \code{YYYY-MM-DD}, or a date range \code{YYYY-MM-DD/YYYY-MM-DD}, and more).
393422
\end{document}
394423
]]
395424

packages/bibtex/styles/chicago.lua

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,55 @@
11
local Bibliography = require("packages.bibtex.bibliography")
22

3+
-- WORKAROUND
4+
-- We would want fluent strings for all languages, and rules for assembling
5+
-- dates in different languages.
6+
-- For now, we just implement English (as "Month Day, Year").
7+
-- The logic here is also very incomplete, as it doesn't handle date ranges,
8+
-- approximate dates, etc.
9+
-- Eventually, we'll switch to CSL, which as provisions for localized dates,
10+
-- so the effort fixing this is not worth it.
11+
-- It's still better that what we had before (raw rendering of the month field).
12+
local MONTHNAMES = {
13+
"January",
14+
"February",
15+
"March",
16+
"April",
17+
"May",
18+
"June",
19+
"July",
20+
"August",
21+
"September",
22+
"October",
23+
"November",
24+
"December",
25+
}
26+
local SEASONNAMES = { "Spring", "Summer", "Fall", "Winter" }
27+
local function fullDate (item)
28+
local d = item.date
29+
if d then
30+
if d.year and d.month and d.day then
31+
return MONTHNAMES[d.month] .. " " .. d.day .. ", " .. d.year
32+
end
33+
if d.year and d.month then
34+
return MONTHNAMES[d.month] .. " " .. d.year
35+
end
36+
if d.year and d.season then
37+
return SEASONNAMES[d.season] .. " " .. d.year
38+
end
39+
if d.year then
40+
return d.year
41+
end
42+
return ""
43+
end
44+
if item.year and item.month then
45+
return MONTHNAMES[item.month] .. " " .. item.year
46+
end
47+
if item.year then
48+
return item.year
49+
end
50+
return ""
51+
end
52+
353
local ChicagoStyles = pl.tablex.merge(Bibliography.Style, {
454
CitationStyle = Bibliography.CitationStyles.AuthorYear,
555

@@ -23,7 +73,7 @@ local ChicagoStyles = pl.tablex.merge(Bibliography.Style, {
2373
italic(journaltitle),
2474
optional(" ", volume),
2575
optional(" no. ", number),
26-
optional(" ", parens(optional(month, " "), year)),
76+
optional(" ", parens(fullDate)),
2777
optional(": ", pageRange),
2878
".",
2979
optional(" ", doi, "."),
@@ -35,8 +85,7 @@ local ChicagoStyles = pl.tablex.merge(Bibliography.Style, {
3585
quotes(title, "."),
3686
" ",
3787
italic(journaltitle),
38-
optional(", ", month),
39-
optional(", ", year),
88+
optional(", ", fullDate),
4089
optional(": ", pageRange),
4190
".",
4291
optional(" ", doi, "."),
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
-- ISO 8601-2 Extended Date/Time Format Level 1 (partial)
2+
-- See https://www.loc.gov/standards/datetime/
3+
-- Not yet supported, but unlikely to be needed in a bibliographic context:
4+
-- Year prefix by Y (before -9999 or after 9999)
5+
-- Unspecified digit(s) from the right (e.g. 202X, 2024-XX, etc.)
6+
7+
-- Examples:
8+
-- "2020-01" (January 2020)
9+
-- "2020-23" (Autumn 2020)
10+
-- "2020-11-01" (November 1, 2020)
11+
-- "2020-12-01T12:13:55+01:23" (December 1, 2020, 12:13:55 PM, UTC+1:23)
12+
-- "2020-12-01T12:13:55Z" (December 1, 2020, 12:13:55 PM, UTC)
13+
-- "2020-12-01T12:13:55" (December 1, 2020, 12:13:55 PM, local considered as UTC here)
14+
-- "2033?" (2033, approximate / circa)
15+
-- "2033-01~" (January 2033, approximate month
16+
-- "2033-01-12%" (January 12, 2033, approximate day)
17+
-- "2033-01-01/2033-01-12" (January 1-12, 2033)
18+
-- "/2033-01-12" (up to January 12, 2033)
19+
-- "2033-01-31/" (from January 31, 2033)
20+
21+
local lpeg = require("lpeg")
22+
local R, S, P, C, Ct, Cg = lpeg.R, lpeg.S, lpeg.P, lpeg.C, lpeg.Ct, lpeg.Cg
23+
24+
local digit = R("09")
25+
local dash = P("-")
26+
local colon = P(":")
27+
local slash = P("/")
28+
local yapprox = P("?") / function ()
29+
return "true"
30+
end
31+
local mapprox = P("~") / function ()
32+
return "true"
33+
end
34+
local dapprox = P("%") / function ()
35+
return "true"
36+
end
37+
-- time
38+
local D2 = digit * digit / tonumber
39+
local offset = P("Z")
40+
+ C(S("+-"))
41+
* C(D2)
42+
* colon
43+
* C(D2)
44+
/ function (s, h, m)
45+
local sign = s == "+" and 1 or -1
46+
return { hour = h * sign, minute = m * sign }
47+
end
48+
local timespec = P("T")
49+
* Cg(D2, "hour")
50+
* colon
51+
* Cg(D2, "minute")
52+
* colon
53+
* Cg(D2, "second")
54+
* Cg(offset ^ -1, "offset")
55+
-- year from -9999 to 9999
56+
local D4 = digit * digit * digit * digit / tonumber
57+
local year = D4 + P(dash) * D4
58+
-- month 01-12
59+
local month = (P("0") * R("19") + P("1") * R("02")) / tonumber
60+
-- season 21-24 (Spring, Summer, Autumn, Winter)
61+
local season = P("2") * R("14") / function (s)
62+
return tonumber(s) - 20
63+
end
64+
-- day 01-31 (unverified)
65+
local day = D2 / tonumber
66+
-- date
67+
local datespec = Cg(year, "year") * Cg(yapprox, "approximate")
68+
+ Cg(year, "year") * (dash * Cg(month, "month") * Cg(mapprox, "approximate"))
69+
+ Cg(year, "year") * (dash * Cg(month, "month") * (dash * Cg(day, "day") * Cg(dapprox, "approximate")))
70+
+ Cg(year, "year") * (dash * Cg(season, "season"))
71+
+ Cg(year, "year") * (dash * Cg(month, "month") * (dash * Cg(day, "day") * timespec ^ -1) ^ -1) ^ -1
72+
local date = Ct(datespec)
73+
/ function (t)
74+
return {
75+
year = t.year,
76+
season = t.season,
77+
month = t.month,
78+
approximate = t.approximate,
79+
day = t.day,
80+
-- N.B Local time does not make sense in a blibliographic context
81+
-- so we ignore the offset and consider all times to be UTC even without a Z
82+
hour = t.hour,
83+
minute = t.minute,
84+
second = t.second,
85+
}
86+
end
87+
local startdate = Ct(date * slash * date ^ -1)
88+
/ function (t)
89+
local approx = t[1].approximate or t[2] and t[2].approximate
90+
return {
91+
approximate = approx,
92+
range = true,
93+
startdate = t[1],
94+
enddate = t[2],
95+
}
96+
end
97+
local enddateonly = slash
98+
* date
99+
/ function (t)
100+
return {
101+
aproximate = t.approximate,
102+
range = true,
103+
enddate = t,
104+
}
105+
end
106+
local dateinterval = startdate + enddateonly
107+
local END = P(-1)
108+
local isodatetimspec = (dateinterval + date) * END
109+
110+
--- Parse an ISO 8601 date/time string.
111+
-- For a single date, the fields are year, month, day, season, hour, minute,
112+
-- second, and approximate (true/false).
113+
-- For date ranges, the start and/or end dates are returned in a table, with
114+
-- the range field set to true for convenience.
115+
-- @tparam string dt The date/time string
116+
-- @treturn table A table with the parsed date/time, or nil if the string could not be parsed
117+
local function isodatetime (dt)
118+
return lpeg.match(isodatetimspec, dt)
119+
end
120+
121+
return isodatetime

0 commit comments

Comments
 (0)