scriban
scriban copied to clipboard
array.sort only works with simple members (can't use nested members)
For instance, if I have a data structure like this:
order --(N)--> line --(1)--> product
It's not possible to do this
{{ for line in order.lines | array.sort "product.name" }}
because array.sort
treats "product.name" as a direct member of the "line" object (IObjectAccessor.TryGetValue
is invoked with "product.name" as the member
parameter).
(the code above doesn't throw an error but the lines aren't sorted)
If I want to display the products in order, I can do this:
{{ for product in order.lines | array.map "product" | array.sort "name" }}
But then I don't have access to the "line" object.
I think array.sort
should access the "name" member of the "product" member of the "line", rather than accessing the "product.name" member of the "line" (which doesn't exist).
Current workaround: replace the current implementation of array.sort
with this:
class CustomBuiltinFunctions : BuiltinFunctions
{
public CustomBuiltinFunctions()
{
var arrayFunctions = (ArrayFunctions)this["array"];
arrayFunctions.Import("sort", new Func<TemplateContext, SourceSpan, object, string, IEnumerable>(Sort));
}
private static IEnumerable Sort(TemplateContext context, SourceSpan span, object list, string member = null)
{
if (list == null)
{
return Enumerable.Empty<object>();
}
var enumerable = list as IEnumerable;
if (enumerable == null)
{
return new ScriptArray(1) { list };
}
var realList = enumerable.Cast<object>().ToList();
if (realList.Count == 0)
return realList;
if (string.IsNullOrEmpty(member))
{
realList.Sort();
}
else
{
var pathSegments = member.Split('.');
realList.Sort((a, b) =>
{
object leftValue = null;
object rightValue = null;
TryGetValueForPath(context, span, a, pathSegments, out leftValue);
TryGetValueForPath(context, span, b, pathSegments, out rightValue);
return Comparer<object>.Default.Compare(leftValue, rightValue);
});
}
return realList;
}
private static bool TryGetValueForPath(TemplateContext context, SourceSpan span, object obj, string[] pathSegments, out object value)
{
value = obj;
foreach (var member in pathSegments)
{
object memberValue;
var accessor = context.GetMemberAccessor(obj);
if (accessor.TryGetValue(context, span, obj, member, out memberValue))
{
obj = memberValue;
}
else if (context.TryGetMember(context, span, obj, member, out memberValue))
{
obj = memberValue;
}
else
{
return false;
}
}
value = obj;
return true;
}
}
I think this would be a breaking change for some people.
Since a "member name" is really just a dictionary key, and dictionary keys can contain periods, we cannot assume that foo.bar
means "the bar
property of the member foo
." It may mean "The member foo.bar
."
I think the best we could do, without a breaking change, is first check to see if there's a member foo.bar
present. If that member is not present (note, this is not the same as checking to see if the result of that evaluation is null), then and only then can we check property bar
on member foo
.
Example:
Template text:
{{ for line in lines | array.sort "product.name" }}
Department: {{ line.department }}
Product Name: {{ line.product.name }}
Aisle: {{ line.product.aisle }}
{{ end }}
Code:
var input = new ScriptObject
{
{
"lines",
new[]
{
new ScriptObject
{
{"product.name", "celery"},
{ "department", "food" },
{
"product", new
{
Name = "sedano",
Aisle = 10
}
}
},
new ScriptObject
{
{ "product.name", "dog" },
{ "department", "pets" },
{
"product",
new
{
Name = "cane",
Aisle = 5
}
}
}
}
}
};
Console.WriteLine(template.Render(input));
Currently results in the following:
Department: food
Product Name: sedano
Aisle: 10
Department: pets
Product Name: cane
Aisle: 5
With your changes, it results in the following:
Department: pets
Product Name: cane
Aisle: 5
Department: food
Product Name: sedano
Aisle: 10
Current workaround: replace the current implementation of
array.sort
with this:class CustomBuiltinFunctions : BuiltinFunctions { public CustomBuiltinFunctions() { var arrayFunctions = (ArrayFunctions)this["array"]; arrayFunctions.Import("sort", new Func<TemplateContext, SourceSpan, object, string, IEnumerable>(Sort)); } private static IEnumerable Sort(TemplateContext context, SourceSpan span, object list, string member = null) { if (list == null) { return Enumerable.Empty<object>(); } var enumerable = list as IEnumerable; if (enumerable == null) { return new ScriptArray(1) { list }; } var realList = enumerable.Cast<object>().ToList(); if (realList.Count == 0) return realList; if (string.IsNullOrEmpty(member)) { realList.Sort(); } else { var pathSegments = member.Split('.'); realList.Sort((a, b) => { object leftValue = null; object rightValue = null; TryGetValueForPath(context, span, a, pathSegments, out leftValue); TryGetValueForPath(context, span, b, pathSegments, out rightValue); return Comparer<object>.Default.Compare(leftValue, rightValue); }); } return realList; } private static bool TryGetValueForPath(TemplateContext context, SourceSpan span, object obj, string[] pathSegments, out object value) { value = obj; foreach (var member in pathSegments) { object memberValue; var accessor = context.GetMemberAccessor(obj); if (accessor.TryGetValue(context, span, obj, member, out memberValue)) { obj = memberValue; } else if (context.TryGetMember(context, span, obj, member, out memberValue)) { obj = memberValue; } else { return false; } } value = obj; return true; } }
thanks for this, it works perfectly but I had to add array.Remove("sort");
to make it work