It’s easier to understand if you inspect some intermediate results.
Add a console.log to see what scripts returns, remove .name to see what the result of the reduce call is:
function dominantDirection(text) {
const scripts = countBy(text, (char) => {
const script = characterScript(char.codePointAt(0));
return (script
? script.direction
: "none"
);
})
.filter(({name}) => name !== "none");
if(scripts.length === 0){
return "ltr";
}
console.log(scripts); // What is the result of the `countBy` function?
return scripts.reduce((a, b) => (a.count > b.count
? a
: b)); // What is the object that the `name` property comes from?
}
Now dominantDirection("Hello!") will log scripts as
[
{ name: "ltr", count: 5 }
]
and the result will also be
{ name: "ltr", count: 5 }
And dominantDirection("Hey, مساء الخير") will log scripts as
[
{ name: "ltr", count: 3 },
{ name: "rtl", count: 9 }
]
with the result
{ name: "rtl", count: 9 }
The scripts array comes from the countBy call which returns a count of how many code points there are in the string, for each script direction.
It tries to find the corresponding script from each of the SCRIPTS by comparing which of the ranges the codePoint falls into and getting the corresponding direction property.
This higher-order function countBy accepts the arguments items and groupName.
dominantDirection calls countBy with two arguments and stores its result in scripts.
items is an iterable value, in this case a string (of code points): that’s just the input string, e.g. "Hey, مساء الخير". From this value, individual items (code points) will be grouped into “buckets” and counted separately.
groupName is a function that returns the name of the “bucket” that a single code point (e.g. a character) belongs to (based on the code point itself): in this case, it’s the arrow function char => {…} which calls characterScript with a single char’s code point and returns the corresponding script object (which you say you understand). It then gets the script’s direction, for example "ltr" for the { name: "Coptic",…} object from your example (or "none" if no script object can be found).
As an aside, groupName isn’t a good name, as it expects a function, but the name implies a string.
Maybe groupNameFromItem is better.
As countBy iterates over the string (for (let item of items)), this function (originally char => {…}) is called and assigned to name (let name = groupName(item);).
Since char => {…} returns a script’s direction, name becomes either "ltr", "rtl", or "none" — that’s the name of the “bucket”.
The array counts is populated with objects like { name: "ltr", count: 1 }.
If the next code point also comes from an ltr script, this object is found using findIndex and its count incremented using ++.
This populated array is then returned (which is what scripts refers to inside dominantDirection).
The reduce is easily explained: a and b are objects from one of the scripts arrays.
If a.count is higher than b.count, a is returned, otherwise b is returned; the returned object is then used for the next comparison, or, if nothing else needs to be compared, returned as a result.
So the reduce call finds the object with the maximum count.
In the original code, only the name is returned at the end, not the entire object.
To summarize:
text is a string that consists of code points from different scripts.
countBy takes text, iterates over the code points, calls groupName to get the “bucket name” for the current code point, populates the counts array (named scripts, outside of the function) with { name, count } entries that tell you that count many code points come from scripts in the name direction.
Then a reduce looks for the maximum count among these entries and its name is returned.
Two more things:
-
I understand how a loop with the some method is used to find any arrays in which the character code returns true.
The character code itself doesn’t return true.
The some call returns true if the code point code falls into any of the ranges between from (inclusive) and to (exclusive), or false, otherwise.
The chapter is about higher-order functions, so it’s important to understand how the groupName parameter in function countBy(items, groupName){…} works.
I’m not quite sure how familiar you are with this concept, but here’s a simpler example where odd and even numbers are counted with some explanatory comments:
const countOddAndEvenNumbers = (iterable) => {
const oddOrEvenBucketFromNumber = (number) => (number % 2 === 0 ? "even" : "odd"); // This is the function that distinguishes odd and even numbers.
return countGroups(iterable, oddOrEvenBucketFromNumber); // The distinguishing function is passed to `countGroups` to be used.
},
countGroups = (iterable, bucketNameFromItem) => {
const result = {}; // Usually counting is done with hash maps, e.g. objects or Maps, instead of arrays.
for(let item of iterable){
const bucketName = bucketNameFromItem(item); // Generic way of `const bucketName = (item % 2 === 0 ? "even" : "odd")`; acts as `const bucketName = oddOrEvenBucketFromNumber(item)`, but with no own knowledge of what odd or even numbers are: it’s entirely separated and knows nothing about the implementation of the function.
result[bucketName] = (result[bucketName] ?? 0) + 1; // Increment entry `bucketName` by one. If it doesn’t exist, initialize it to `0` first.
}
return result;
};
countOddAndEvenNumbers([0, 1, 1, 2, 3, 5, 8, 13]); // { "even": 3, "odd": 5 }