After playing for a while I have something that works for me.
The following template processes the books displayed on the book list, in the order they are displayed. The template has two arguments: "field name" and "item count". It looks at "field name" in each book (in order). If the book has an "item value" in that field, it checks if it already has found "item count" books for that "item value". If it has then it ignores the book. If it hasn't then it adds the book to the list of books to display.
Spoiler:
Code:
python:
def evaluate(book, context):
from collections import defaultdict
if context.arguments is None or len(context.arguments) != 2:
raise ValueError("This template requires two arguments, field_name and item_count")
field_name = context.arguments[0]
desired_count = int(context.arguments[1])
data_view = context.db.data
db = context.db.new_api
# Check if we have already computed the necessary data
book_ids = context.globals.get('book_ids', None)
if book_ids is None:
# The candidates are the books matched by any previous search expression
candidates = context.globals.get('_candidates', db.all_book_ids())
books = defaultdict(set)
answer = set()
# Scan the library from top to bottom, in the order being displayed. This
# reflects what is currently shown in the GUI, such as previous searches or VLs.
for book_id in (tr.book_id for tr in data_view):
# Check if this book is allowed by previous search terms
if book_id not in candidates:
continue
# Get the value(s) to be checked.
item_values = db.field_for(field_name, book_id)
if item_values is None:
continue
if isinstance(item_values, str):
# We have a single value, such as a publisher or series.
# Make it into a list of one value
item_values = (item_values,)
for item_val in item_values:
# Check if we have seen this item value before.
item_set = books[item_val]
if len(item_set) < desired_count:
# We haven't hit the limit of books with this item value to display
item_set.add(book_id)
answer.add(book_id)
# Save the computed results to be used for the rest of the books being examined
context.globals['book_ids'] = answer
# Check if the current book is to be displayed
book_ids = context.globals['book_ids']
return '1' if book.id in book_ids else ''
Because the book list is scanned in displayed order, how the book list is sorted is important. The "item values" are checked in their order of appearance. This lets you control which books you get, for example:
- Highest or lowest series index.
- Find series with more than 5 books then show the book with the lowest series index. (Uses this template search for the Find series part.)
- Tags with the highest or lowest rating.
- Publishers with highest or lowest rating.
- Authors of books with a series that has notes, in genre order.
- And so on.
Basically, if you can sort the book list into the order you want, this template will respect that order, showing "item count" books having "field name" with some "item value".
Although not strictly required, the template works much better if running from latest source. If you don't then search caching can get involved, showing you some previous result instead of a current calculation. The change will be in the next release/preview.
The following example will show the first book of each series that has a rating.
- As above, create a stored template containing the template code in the spoiler. Name it whatever you want. I used "summarize_booklist". Ignore the ValueError shown in the Stored templates dialog.
- Use search to find the list of books you want to process. I recommend you make a temporary VL for the search once you have what you want. Example: rating:true
- Sort the book list in the order you want books to be examined. Example: series ascending.
- Using Advanced search, make a template search. Example: lowest series index. Because I used the name "summarize_booklist", the search looks like this:
The template is
Code:
program: summarize_booklist('series',1)