///|
fn sort_value(item : Item, key : SortKey, style : Style) -> String {
if !key.variable.is_empty() {
if item.date(key.variable) is Some(date) {
return if date.year == 0 { "" } else { date_sort_stamp(date).to_string() }
}
let names = item.name(key.variable)
if !names.is_empty() {
return join_nonempty(
names.map(fn(name) {
name_family_key(name) + " " + name_given_key(name)
}),
";",
)
}
item_value(item, key.variable)
} else if !key.macro_name.is_empty() {
match style.macros.get(key.macro_name) {
Some(xs) => render_nodes(xs, item, style, "text")
None => ""
}
} else {
item.id
}
}
///|
fn sorted_items(
items : Array[Item],
keys : Array[SortKey],
style : Style,
) -> Array[Item] {
let out = items.copy()
if keys.is_empty() {
return out
}
let order = Map([])
for i = 0; i < items.length(); i = i + 1 {
order[items[i].id] = i
}
out.sort_by(fn(a, b) {
let result = compare_items(a, b, keys, style)
if result == 0 {
cmp_int(order.get(a.id).unwrap_or(0), order.get(b.id).unwrap_or(0))
} else {
result
}
})
out
}
///|
fn compare_items(
a : Item,
b : Item,
keys : Array[SortKey],
style : Style,
) -> Int32 {
for key in keys {
let left = sort_value(a, key, style).to_lower()
let right = sort_value(b, key, style).to_lower()
// Missing keys go last in both ascending and descending sorts.
if left.is_empty() && !right.is_empty() {
return 1
}
if right.is_empty() && !left.is_empty() {
return -1
}
let c = match (parse_int(left), parse_int(right)) {
(Some(x), Some(y)) => cmp_int(x, y)
_ =>
if a.date(key.variable) is Some(x) {
if b.date(key.variable) is Some(y) {
if date_sort_stamp(x) < date_sort_stamp(y) {
-1
} else if date_sort_stamp(x) > date_sort_stamp(y) {
1
} else {
0
}
} else {
cmp_str(left, right)
}
} else {
cmp_str(left, right)
}
}
if c != 0 {
return if key.sort_order == "descending" { -c } else { c }
}
}
0
}
///|
fn copy_render_item(item : Item) -> Item {
// Only the copied maps are changed; caller-owned names and dates stay read-only.
{
..item,
strings: item.strings.copy(),
numbers: item.numbers.copy(),
names: item.names.copy(),
dates: item.dates.copy(),
booleans: item.booleans.copy(),
}
}
///|
fn prepare_items(
items : Array[Item],
style : Style,
) -> Result[Array[Item], CslError] {
let ids = Map([])
let used = Map([])
let copied = []
for i = 0; i < items.length(); i = i + 1 {
let item = items[i]
if item.id.is_empty() || ids.contains(item.id) {
return Err(BadItem("empty or duplicate item id: " + item.id))
}
for variable, date in item.dates {
if date_is_range(date) || !date.literal.is_empty() {
return Err(
BadItem("unsupported date range/literal: " + item.id + "/" + variable),
)
}
if date.month < 0 || date.month > 12 || date.day < 0 || date.day > 31 {
return Err(
BadItem(
"date component out of supported bounds: " +
item.id +
"/" +
variable,
),
)
}
}
ids[item.id] = true
let copy = copy_render_item(item)
let explicit = item_value(item, "citation-number")
if !explicit.is_empty() {
let n = match parse_int(explicit) {
Some(n) if n > 0 => n
_ => return Err(BadItem("invalid citation-number: " + explicit))
}
if used.contains(n) {
return Err(BadItem("duplicate citation-number: " + explicit))
}
used[n] = true
copy.strings["citation-number"] = n.to_string()
} else {
copy.strings["citation-number"] = (i + 1).to_string()
}
copied.push(copy)
}
let ordered = sorted_items(copied, style.bibliography_sort, style)
let mut next = 1
for item in ordered {
let original = item_by_id(items, item.id).unwrap()
if item_value(original, "citation-number").is_empty() {
while used.contains(next) {
next = next + 1
}
item.strings["citation-number"] = next.to_string()
used[next] = true
next = next + 1
}
}
// Limited author/year disambiguation works on copies, never on caller data.
if style.citation_disambiguate {
match assign_rendered_suffixes(ordered, style) {
Err(e) => return Err(e)
Ok(_) => ()
}
}
Ok(ordered)
}