Jekyll: sort collections by size - jekyll

I want to sort my Jekyll collections by the number of documents that are in each collection.
Each collection in the site.collections variable has a docs field, and the docs field (which is an array of documents) has a size field, which is the number of documents in this collection (see documentation).
However, something like this doesn't work:
{% assign sorted = site.collections | sort: 'docs.size' %}
{% for coll in sorted %}
...
{% endfor %}
It results in a
Liquid Exception: no implicit conversion of String into Integer
It seems that the argument to sort can only be an immediate field of the type of object being sorted, and not a field of a field thereof.
Is there a way to achieve sorting the collections by the number of documents they contain?

Build an array of the available sizes:
{% assign sorted = '' | split: "" %}
{% for coll in site.collections %}
{% assign sorted = sorted| append: coll.docs.size %}
{% endfor %}
Sort the above array.
Iterate the above array and all your collections printing only the collection whose size matches the sorted array number.

Ok, I achieved it in a rather ugly way, along the lines of marcanuy's answer.
<!-- Create a comma-separated string of all the sizes of the collections -->
{% for coll in site.collections %}
{% if coll.title %}
{% if coll.docs.size < 10 %}
{% assign str = coll.docs.size | prepend: "00" %}
{% elsif coll.docs.size < 100 %}
{% assign str = coll.docs.size | prepend: "0" %}
{% else %}
{% assign str = coll.docs.size %}
{% endif %}
{% assign sizes = sizes | append: str | append: "," %}
{% endif %}
{% endfor %}
<!-- Remove last comma of string -->
{% assign length = sizes | size | minus: 1 %}
{% assign sizes = sizes | slice: 0, length %}
<!-- Split string into array, sort DESC, and remove duplicate elements -->
{% assign sizes = sizes | split: "," | sort | reverse | uniq %}
<!-- Iterate through sizes, and for each size print those collections that have this size -->
{% for s in sizes %}
{% for coll in site.collections %}
{% assign i = s | plus: 0 %}
{% if coll.docs.size == i %}
<p>{{ coll.title }}: {{ i }} documents</p>
{% endif %}
{% endfor %}
{% endfor %}
The main difficulty is that an array of sizes created like this, is an array of strings, and sorting it results in an alphabetical sort order, rather than in an numerical one (e.g. "15" comes before "2").
To remedy this, I prepend "00" to numbers less than 10, and "0" to number less than 100. This makes the alphabetical sort order coincide with the desired numerical sort order.
Then I iterate through these sizes (which are still strings), and convert them to integers on the fly (by plus: 0) so that I can compare them to the docs.size field of each collection.
It's pretty verbose, but since this is executed only when the site is generated, and not at each request in production mode, it's ok.
Still, better solutions are welcome!

Related

How to create or define a dictionary in liquid templates?

According to shopify,
The only possible types of objects that be created are:
String
Number
Boolean
Nil
Array
EmptyDrop
Any workarounds that anyone might be aware of?
Duplicating a workaround from github, (For more visibility)
Emulating / Fake dictionary workaround
Example:
Declaring:
{% assign rgbColors = "red:#FF0000,green:#00FF00,blue:#0000FF" | split: "," %}
{% assign value = "green" %}
Acessing:
{% for color in rgbColors %}
{% assign colorKeyVal = color | split ":" %}
{% assign colorKey = colorKeyVal[0] %}
{% assign colorValue = colorKeyVal[1] %}
{% if colorKey == value %}
<div style="background: {{ colorValue }}"></div>
{% endif %}
{% endfor %}

Liquid filter collection where not null

In my front matter for some pages (not all) I have:
---
top-navigation:
order: 2
---
Using liquid I want to filter all site pages which have a top-navigation object and sort by top-navigation.order.
I'm trying sort:'top-navigation.order' but that's throwing an exception of undefined method [] for nil:NilClass. I tried where:"top-navigation", true but it's not equating truthy values.
How can I filter for pages that have top-navigation and then sort?
Two steps:
Create an array with pages that contains the top-navigation key.
We create an empty array and then push the items that have the key.
{% assign navposts = ''|split:''%}
{% for post in site.posts %}
{% if post.top-navigation %}
{% assign navposts = navposts|push:post%}
{% endif %}
{% endfor %}
Sort the above array by top-navigation.order
{% assign navposts = navposts|sort: "top-navigation.order"%}
Printing results:
{% for post in navposts %}
<br>{{ post.title }} - {{post.top-navigation}}
{% endfor %}
For pages use site.pages.
In Jekyll 3.2.0+ (and Github Pages) you can use the where_exp filter like so:
{% assign posts_with_nav = site.posts | where_exp: "post", "post.top-navigation" %}
Here, for each item in site.posts, we bind it to the 'post' variable and then evaluate the expression 'post.top-navigation'. If it evaluates truthy, then it will be selected.
Then, putting it together with the sorting, you'd have this:
{%
assign sorted_posts_with_nav = site.posts
| where_exp: "post", "post.top-navigation"
| sort: "top-navigation.order"
%}
Liquid also has the where filter which, when you don't give it a target value, selects all elements with a truthy value for that property:
{% assign posts_with_nav = site.posts | where: "top-navigation" %}
Unfortunately, this variant does not work with Jekyll.

'where' not finding entries given a parameter to look for in CSV data

jekyll 2.4.0, Mac 10.12.5
{% for year_of_interest in (1997..2017) reversed %}
<large_year>{{year_of_interest}}</large_year>
{% for paper in site.data.publications | where,'site.data.publications.Year',year_of_interest %}
<div class="publication_card">
<a class="article_title" href="../../{{paper.Link}}" title="{{paper.Abstract}}">{{paper.Title}}</a>
</div>
<div class="paper_author_container">
<span class="paper_authors">{{paper.Author | upcase}}</span>
<br>
<span class="journal_info">{{paper.Year}}—{{paper.Journal | upcase}}</span>
<button class="btn" data-clipboard-text="{{paper.BibTex}}">
BIBTEX
</button>
</div>
{% endfor %}
{% endfor %}
The input CSV has this shape and the Year is a simple number:
Title,Link,Abstract,Author,BibTex,Year,Journal,SupplementalLink
background: I'm stuck! I have a CSV where each row represents publication metadata for papers from 1997 to 2016. Some years have many papers, but each year has at least 1 publication. I want a header for each year, and the publications to be posted below. Unfortunately, the where filter does not find any of the articles for a given year in the for loop.
Current functionality: under each header, it shows a list of ALL publications.
Desired: it should only show publications where the paper.Year == year_of_interest.
Thanks in advance!
Three problems here :
You can't filter in a loop
{% for paper in site.data.publications | where,'site.data.publications.Year', year_of_interest %}
Will not work as expected because it always returns all datas.
{% assign filtered = site.data.publications | where,'site.data.publications.Year', year_of_interest %}
{% for paper in filtered %}
Will work, but not now ...
Where filter filters on a key
It's not {% site.data.publications | where,'site.data.publications.Year', year_of_interest %}
but : {% site.data.publications | where,'Year', year_of_interest%}}
Nearly working ...
CSV datas are strings
{{ site.data.publications[0].Year | inspect }} returns "1987" and double quotes around signifies that its a string and that your filter, looking for an integer as "Year" value will never find it. You have to look for a string instead.
To cast an integer into a string you can append an empty string to it.
{% for year_of_interest in (1997..2017) reversed %}
{% comment %} casting an integer to a string {% endcomment %}
{% assign yearAsString = year_of_interest | append:"" %}
{% comment %} filtering datas {% endcomment %}
{% assign selectedEntries = site.data.publications | where: "Year", yearAsString %}
{% for paper in selectedEntries %}
Now, it does the job.
Notes :
1 - Use the | inspect filter to debug, it's useful to determine type of value (string, integer, array, hash).
2 - You can also cast a string to an integer by adding zero to it :
{% assign numberAsString = "1997" %}
{{ numberAsString | inspect }} => "1997"
{% assign numberAsInteger = numberAsString | plus: 0 %}
{{ numberAsInteger | inspect }} => 1997
This is the only documentation for the where filter because it is not a default liquid filter.
https://gist.github.com/smutnyleszek/9803727
site.data.publication.Year is an object of site.data.publications I believe you only need to specify "Year" This is case sensitive by the way.
{% for paper in site.data.publications | where, "Year", year_of_interest %}

jekyll assign concat in a loop?

I would like to organize a page based on the number of pages that pass a filter.
I have tried to append truthy pages to a collection but it doesn't work.
{% assign homepage_posts = [] %}
{% for my_page in site.pages %}
{% if my_page.homepage %}
{% assign homepage_posts = homepage_posts | concat: [my_page] %}
{% endif %}
{% endfor %}
<h1>size{{homepage_posts.size}}</h1>
<h1>{{homepage_posts}}</h1>
This is not working. Does concat only work with strings?
Jekyll will use Liquid 4 soon. But, for now, no concat.
In your case you can :
Create an empty array (bracket notation doesn't work in liquid) : {% assign homepage_posts = "" | split:"/" %}
{{ homepage_posts | inspect }} --> output : []
And push elements in it :
{% for my_page in site.pages %}
{% if my_page.homepage %}
{% assign homepage_posts = homepage_posts | push: mypage %}
{% endif %}
{% endfor %}
{{ homepage_posts | inspect }}
concat filter only works with arrays and will be available in Jekyll when it upgrades to Liquid 4.*:
concat
Concatenates (combines) an array with another array. The resulting
array contains all the elements of the original arrays. concat will
not remove duplicate entries from the concatenated array unless you
also use the uniq filter.
To filter pages containing a specific attribute (in this case homepage: true) you can use a where filter.
Having a page with front matter:
---
homepage: true
---
Then you can have the pages with the homepage: true attribute like:
{% assign homepages = site.pages | where:"homepage","true" %}

Jinja2 for loop behaving similarly to with

I'd like to iterate over a set of objects and find the maximum of one particular attribute, however jinja2 ignores any action within an iterator on a variable declared outside of the iterator. For example:
{% set maximum = 1 %}
{% for datum in data %}
{% if datum.frequency > 1 %}
{% set maximum = datum.frequency %}
{% endif %}
{% endfor %}
{# maximum == 1 #}
datum.frequency is definitely greater than 1 for some datum in data.
EDIT (solution)
This is similar to this post, but there's a bit more to it. The following works and is very ugly.
{% set maximum = [1] %}
{% for datum in data %}
{% if datum.freq > maximum[-1] %}
{% if maximum.append( datum.freq ) %}{% endif %}
{% endif %}
{% endfor %}
{% set maximum = maximum[-1] %}
Have you considered writing a custom filter to return the highest value of a particular attribute within your collection? I prefer to minimize the amount of logic I use in Jinja2 templates as part of maintaining a 'separation of concerns'.
Here is a link to a very good example of how one can be written in python:
Custom jinja2 filter for iterator
Once you have your filter returning the value you need access it by using '|' like so:
{% set maximum = datum|filtername %}