JavaScript: Sentence variations using the reduce function - javascript

For search purposes, given a string like BBC Sport I want to construct an array that looks like:
[ 'BBC', 'BB', 'B', 'Sport', 'Spor', 'Spo', 'Sp', 'S' ]
I've implenented it using 2 for loops:
const s = "BBC sport";
const tags = [];
const words = s.split(" ");
for (let word of words) {
const wl = word.length;
for (let i = 0; i < wl; i++) {
tags.push(word.substr(0, wl - i));
}
}
// tags now equals [ 'BBC', 'BB', 'B', 'Sport', 'Spor', 'Spo', 'Sp', 'S' ]
However, I'd like to implement it, if possible, with the reduce function instead of for loops.
How would you solve it?

Honestly I'd write the code the way you did. Two loops are readable, maintainable and fast.
If you really need a oneliner:
s.split(" ").flatMap(word => Array.from(word, (_, i) => word.slice(0, i + 1)))

Here is a solution relying on function generators (which I would use) and a solution with reduce (as you asked) (which I wouldn't personally use), accepting an input string and a separator.
In your case, the separator is blankspace, of course, but it can be customized.
The below code will iterate through the input string and slice the relevant part of the string for each occurrence, by capitalizing it (since it looks like you are).
This should be elastic enough and, at the same time, easy to customize by eventually adding additional parameters to the toTagList method, or allowing further transformations since it's iterable.
const s = "BBC sport";
function* toTagList(input, separator) {
// split by the separator.
for (const block of input.split(separator)) {
// For each string block, split the whole word.
var splitted = block.split('');
// slice the input array by taking from the first character to the last one, then decrease to get only the previous portions of said word.
for (var i = splitted.length; i > 0; i--) {
// Finally, yield the capitalized string.
yield capitalize(splitted.slice(0, i).join(''));
}
}
}
// this just capitalizes the string.
function capitalize(input) {
return input.charAt(0).toUpperCase() + input.substring(1, input.length);
}
console.log([...toTagList(s, ' ')]);
If you really want to do that with reduce:
const s = "BBC sport";
const tags = s.split(' ').reduce((acc, next) => {
return acc.push(...Array.from({length: next.length}).map((_, i) => {
return (next.split('').slice(0, i + 1)).join('')
})), acc;
}, [])
console.log(tags);

Related

Get number of words in an array starting with certain characters

I'm trying to solve a problem on a testing website and I'm getting confused on trying to find a way to match the letters of a passed in array wordlist.
In the below example it should be returning 2, as ye should be matched twice - in yesterday and yellow.
Without using a RegEx what would be the best way be to go about getting a passed in wordlist to return a number amount based on the 2nd argument input?
This is the code I have so far, that isn't working:
let wordList = ['yesterday', 'develop', 'environment', 'yellow', 'yikes', 'envious']
const countingLetters = (words, inputStart) => {
let total = 0;
for (let i = 0; i < words.length; i++) {
if (inputStart == words[i]) {
total += 1;
}
}
return total;
};
console.log(countingLetters(wordList, 'ye'));
To do what you require without a regex, you can use the filter() function, to search the array by a given condition, and the indexOf() method to ensure that the match is at the start of the string:
let wordList = ['yesterday', 'develop', 'environment', 'yellow', 'yikes', 'envious']
const countingLetters = (words, inputStart) =>
words.filter(word => word.indexOf(inputStart) === 0).length;
console.log(countingLetters(wordList, 'ye'));

Split an array of strings with each string containing more than one space

I have an array of strings like this. Both left and right portions in the string are separated with spaces (More than 1 space for each).
const arr = [
'A1789 Other tuberculosis of nervous system',
'A179 Tuberculosis of nervous system, unspecified',
'A1801 Tuberculosis of spine'
];
I need to turn this into an array of objects like this, with the first portion as the key and the second portion as the value of the key.
const arrOfObj = [
{ A1789: 'Other tuberculosis of nervous system' },
{ A179: 'Tuberculosis of nervous system, unspecified' },
{ A1801: 'Tuberculosis of spine' }
];
I would split by space, assuming your key cannot contain space. So we'll have first item your key and the "rest" your value, which we can trim
arr.map(s => {
const [key, ...value] = s.split(" ");
return { [key]: value.join(" ").trim() }
})
I'd think something like this would work:
const arrOfObj = {};
arr.forEach(item => {
const match = str.match(/^([A-Za-z0-9]+)\s+(.+)/);
arrOfObj[match[1]] = match[2];
});

How to exclude redundant patterns among a large array of glob string

I have been working on this algorithm for days, and no idea how to figure out the most suitable/easy/optimized solution.
Here I have a large array of string as the followings
[
*.*.complete
*.*.read
*.*.update
*.order.cancel
accounting.*.delete
accounting.*.update
accounting.*.void
accounting.account.*
admin.user.read
admin.user.update
admin.format.delete
...
]
// the array may be in random order
all the values are in some wildcard patterns (in fact, they are the permissions for my system)
what i want to do is to remove redundant patterns, for example: admin.json_api.read is redundant due to *.*.read
can someone give me any suggestion/approach?
The following approach takes different glob segment length's into account as well.
Thus in a first step the glob-array is reduced into one or more segment-length specific arrays of better inspectable glob-items.
Such an item features e.g. a regex specific pattern of its actual glob-value.
Within a final tasks each segment-length specific array gets sanitized separately into an array of non redundant glob-values.
The latter gets achieved by 1st sorting each array descending by each item's glob-value (which assures a sorting from the more to the less generic glob values) and 2nd by rejecting each item where its glob-value gets covered already by a more generic glob-value.
And the base of such a detection is the glob-value specific regex where the asterisk wild card translates into a regex pattern with the same meaning ... thus any glob value of '*.' equals a regex of /[^.]+\./ and any terminating '.*' equals a regex of /\.[^.]+/.
Since the sanitizing task is done via flatMap, the end result is a flat array again ...
function createGlobInspectionItem(glob) {
const segments = glob.split('.');
return {
value: glob,
pattern: glob
.replace((/\*\./g), '[^.]+.')
.replace((/\.\*$/), '.[^.]+')
.replace((/(?<!\^)\./g), '\\.'),
segmentCount: segments.length,
};
}
function collectGlobInspectionItems({ index, result }, glob) {
const globItem = createGlobInspectionItem(glob);
const groupKey = globItem.segmentCount;
let groupList = index[groupKey];
if (!groupList) {
groupList = index[groupKey] = [];
result.push(groupList);
}
groupList.push(globItem);
return { index, result };
}
function createSanitizedGlobList(globItemList) {
const result = [];
let globItem;
globItemList.sort(({ value: aValue }, { value: bValue }) =>
(aValue > bValue && -1) || (aValue < bValue && 1) || 0
);
while (globItem = globItemList.pop()) {
globItemList = globItemList.filter(({ value }) =>
!RegExp(globItem.pattern).test(value)
);
result.push(globItem);
}
return result.map(({ value }) => value);
}
const sampleData = [
// 3 segments
'*.*.complete',
'*.*.read',
'*.*.update',
'*.order.cancel',
'accounting.*.delete',
'accounting.*.update',
'accounting.*.void',
'accounting.account.user',
'accounting.account.*',
'accounting.account.admin',
'admin.user.read',
'admin.user.update',
'admin.format.delete',
// 2 segments
'*.read',
'*.update',
'user.read',
'user.update',
'format.delete',
'format.account',
];
console.log(
'... intermediata inspection result grouped by section length ...',
sampleData
.reduce(collectGlobInspectionItems, { index: {}, result: [] })
.result
);
console.log(
'... final sanitized and flattened glob array ...',
sampleData
.reduce(collectGlobInspectionItems, { index: {}, result: [] })
.result
.flatMap(createSanitizedGlobList)
);
.as-console-wrapper { min-height: 100%!important; top: 0; }
General idea:
Each your pattern can be transformed into regex using:
new RegExp('^' + pattern
.replace(/[./]/g, '\\$&') // escape chars (list isn't full)
.replace(/\*/g, '(.*)') // replace asterisk with '(.*)' - any char(s)
+ '$') // match only full pattern
If one pattern match another one - you don't need both, because pattern with * include second one:
if (pattern1.include('*') && pattern1.test(pattern2)) {
// delete pattern2
}
Simple realization can be found below (still need to optimize a bit).
Full code:
// Your initial array
const patterns = [
'*.*.complete',
'*.*.read',
'*.*.update',
'*.order.cancel',
'accounting.*.delete',
'accounting.*.update',
'accounting.*.void',
'accounting.account.*',
'admin.user.read',
'admin.user.update',
'admin.format.delete',
]
// Build a new one with regexes
const withRegexes = patterns.map(pattern => {
// Create a regex if pattern contain asterisk
const regexp = pattern.includes('*') ? new RegExp('^' + pattern
.replace(/[./]/g, '\\$&')
.replace(/\*/g, '(.*)')
+ '$') : null;
return { pattern, regexp };
});
// Array of indexes of elements where it's pattern already matched by another pattern
let duplicateIndexes = [];
for (let i = 0; i < withRegexes.length - 1; i++) {
for (let j = i + 1; j < withRegexes.length; j++) {
if (withRegexes[i].regexp
&& withRegexes[i].regexp.test(withRegexes[j].pattern)) {
duplicateIndexes.push(j);
}
}
}
// Get unique indexes to delete in desc order
duplicateIndexes = [ ...new Set(duplicateIndexes) ].sort((a, b) => b - a);
// Clear up initial array
for (let index of duplicateIndexes) {
patterns.splice(index, 1);
}
// New one
console.log(patterns);

How to get the length of every word in a string [duplicate]

This question already has answers here:
How to get the length of words in a sentence?
(4 answers)
Closed 3 years ago.
I'm new to coding and have been given this question to solve.
The question I have been given is this;
Create a function that takes a string and returns an array of the lengths of each word in the string.
E.g. 'pineapple and black bean curry' => [9, 3, 5, 4, 5]
The code that I have written is this;
function getWordLengths(str) {
let len = []
let words = str.split()
for (let i = 0; i < words.length; i++){
len.push(words[i])}
return len
}
My code will be run against this test;
describe("getWordLengths", () => {
it("returns [] when passed an empty string", () => {
expect(getWordLengths("")).to.eql([]);
});
it("returns an array containing the length of a single word", () => {
expect(getWordLengths("woooo")).to.eql([5]);
});
it("returns the lengths when passed multiple words", () => {
expect(getWordLengths("hello world")).to.eql([5, 5]);
});
it("returns lengths for longer sentences", () => {
expect(getWordLengths("like a bridge over troubled water")).to.eql([
4,
1,
6,
4,
8,
5
]);
});
});
Dose anyone have any suggestion of haw to make my code work?
You can use string#split and then use map to get the length of each word.
let string = 'pineapple and black bean curry',
result = string.split(/\s+/).map(({length}) => length);
console.log(result)
I changed .split() to .split(' ') and len.push(words[i]) to len.push(words[i].length).
const text = 'pineapple and black bean curry'; //[9, 3, 5, 4, 5]
function getWordLengths(str) {
let len = [];
let words = str.split(' ');
for (let i = 0; i < words.length; i++){
len.push(words[i].length);
}
return len;
}
console.log(getWordLengths(text));
You are not passing any separator to your split function.
So, split the string at every " ", then calculate the length of each word from the resulting array using map:
let string = 'pineapple and black bean curry';
console.log(string.split(' ').map(a => a.length))
\b in a regex expression to split words.
let line='pineapple and black bean curry';
let results=line.match(/\b[^\s]+?\b/g).map(r=>r.length);
console.log(results)
There are quite a few ways to do this buddy.
The simplest way would probably be to do as you attempted, i.e., to use the .split() feature. It works only when used rightly. You have forgotten to split the string based on a separator. And here since you are interested in individual words, the separator should be a space. ' '. just map the lengths of the words onto a new array and print it out.
let string = 'pineapple and black bean curry';
let words = string.split(' ');
console.log(words.map(word => word.length))
If you don't want to use map or split functionalities and want to do it using straightforward manual ways, you can do the following: In this , you just keep a counter variable which you increment as long as you don't encounter a space or end of the string and then reset it at the start of the next word.
let string = 'pineapple and black bean curry';
let count = 0, count_array = [];
let x = string.length;
for(i=0;i<=x;i++){
if(string[i]==' ' || string[i]=='\0' || i==x){
count_array.push(count);
count=0;
}
else{
count++;
}
}
console.log(count_array);

Join sequence of arrays with array delimeters ("intersperse")

Is there a function that lets me concat several arrays, with delimiters between them (the delimiters are also arrays), similarly to how join works but not restricted to strings?
The function can be standard JS or part of a major library such as lodash (which is why it's referenced in the tags).
Here is an example of usage:
let numbers = [[1], [2], [3]];
let result = _.joinArrays(numbers, [0]);
console.log(result);
//printed: [1, 0, 2, 0, 3]
This is analogous to:
let strings = ["a", "b", "c"];
let result = strings.join(",");
console.log(result);
//printed: "a,b,c";
However, join can't be used because it turns values into strings, which I don't want to happen.
But it works for any type.
You could simply use array.reduce to concat the arrays, and push what ever you want to use as your delimiter.
let numbers = [[1], [2], [3]];
let n = numbers.reduce((a, b) => a.concat(0, b))
console.log(n)
Matrix Interspersion
Here's the full monty. Go nuts.
var numbers = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
],
delimiters = [
',', '-', 'x'
];
// matrix interspersion, delimiters into numbers's children
// the rank/order/whatevs of the matrix can be arbitrary and variable
numbers.forEach((x, i) => {
for (var j = 1, l = x.length; j <= l; j+=2 )
x.splice(j, 0, delimiters[i]);
})
alert( "Matrix interspersed: " + JSON.stringify(numbers) );
// normal interspersion, a static delimiter into numbers
for (var j = 1, l = numbers.length; j <= l; j+=2 )
numbers.splice(j, 0, ' AND ');
alert( "Outer array interspersed: " + JSON.stringify(numbers) );
// flattening a 2 rank array into a single array
var flattened = Array.prototype.concat.apply([], numbers);
alert( "Flattened: " + JSON.stringify(flattened) );
var result = [].concat.apply([], numbers);
console.log(result)
Here is an implementation of a function that does this, with some extra logic, in case someone else wants to do it. I was really asking about whether this exists already.
Requires lodash.
export function intersperse(arrs, delimeter) {
let joined = [];
for (var i = 0; i < arrs.length; i++) {
let arr = arrs[i];
if (!arr) continue; //handle sparse arrays
joined.push(...arr);
if (i === arrs.length - 1) break;
if (_.isFunction(delimeter)) {
joined.push(...delimeter());
} else if (_.isArray(delimeter)) {
joined.push(...delimeter);
} else {
throw new Error("unknown type");
}
}
return joined;
}
You could use Array#reduce and return an array with the items and glue if necessary.
const join = (array, glue) => array.reduce((a, b) => a.concat(glue, b));
var numbers = [[1], [2], [3]];
console.log(join(numbers, [0]));
console.log(join(numbers, [42, 43]));
console.log(join([[1]], [0]));
.as-console-wrapper { max-height: 100% !important; top: 0; }
Many good answers here including 4Castle's comments. As for a change I would like to develop a generic Array.prototype.intersperse() for this job.
In functional JS I could come up with 3 alternatives for this job;
Array.prototype.intersperse_1 = function(s){
return this.reduce((p,c,i) => (p[2*i]=c,p), new Array(2*this.length-1).fill(s));
};
Array.prototype.intersperse_2 = function(s){
return this.reduce((p,c,i) => (i ? p.push(s,c) : p.push(c),p),[]);
};
Array.prototype.intersperse_3 = function(s){
return this.reduce((p,c,i) => i ? p.concat([s],[c]) : p.concat([c]),[]);
};
You should stay away from the 3rd one since .concat() is one of the most expensive operations in functional JS. It will not be able to complete the job for even 100K items.
On the other hand while always being slightly faster in small size arrays, the 1st one turns out to be 2x or even more faster than the 2nd in very large arrays both in FF and Chrome. i.e. 1st intersperses a 10M item array in less than 1000 msec while for the same job the 2nd takes like 2000-2500 msec. Handling this size of course wouldn't be possible with 3rd at all.
So in this particular case it will probably be more expensive compared to the tailored solutions since we have to map the result into primitive values but i guess it's still worth noting the following code. I am sure .concat() adopting tailored solutions will fall behind this when the array length is beyond a certain figure.
Array.prototype.intersperse = function(s){
return this.reduce((p,c,i) => (p[2*i]=c,p), new Array(2*this.length-1).fill(s));
}
var arr = [[1],[2],[3]],
result = arr.intersperse([0])
.map(e => e[0]);
console.log(JSON.stringify(result));
As already mentioned here a few times, the simplest way to do this is
function intercalate(glue, arr){
return arr.reduce((acc, v) => acc.concat(glue, v));
}
but this is not the best way, since it creates with every iteration a new (intermediate) array, that is then thrown away. This doesn't matter for this short array of values, but if you ever intend to use this on a longer Array, you might notice the impact.
Better would be to create one Array and push the values into that, as they come in.
function intercalate(glue, arr){
const push = (acc, v) => (Array.isArray(v)? acc.push(...v): acc.push(v), acc);
return arr.reduce((acc, v, i) => push(i===0? acc: push(acc, glue), v), []);
}
But since this Array is gradually increasing it may still need to allocate a bigger chunk of memory and copy the data. These tasks ar very performant, but still unnecessary (imo); we can do better.
We first create a list containing all Arrays and the delimiter in between, and flatten this by using concat.apply([], list). Therefore we produce one intermediate Array, who's size we can compute ahead of time, and the rest is the problem of Array.concat, and it's underlying implementation.
function intersperse(delimiter, arr){
if(!arr.length) return [];
let j = 0, push = (acc, v) => (acc[j++] = v, acc);
return arr.reduce((acc, v) => push(j===0? acc: push(delimiter, glue), v), Array(2*arr.length-1));
}
//or
function intersperse(delimiter, arr){
if(!arr.length) return [];
var out = Array(2*arr.length-1);
out[0] = arr[0];
for(var i=1, j=1; j<out.length;){
out[j++] = delimiter;
out[j++] = arr[i++];
}
return out;
}
//and
function intercalate(glue, arr){
var emptyArray = [];
return arr.length?
emptyArray.concat.apply(emptyArray, intersperse(glue, arr)):
emptyArray;
}
Wich version will be the best/fastest, in the end, is not that easy to tell, since it may depend on the passed values, and wether or not the JIT compiler optimizes the s*** out of it. I made my points, it's up to you to choose wich version/implementation you use.
To the first version: you may prefer to not put this into a lib at all, but write it mostly inline (it's short and simple enough for that), therefore the JIT-compiler may not try to find some common types between the different calls (#monomorphic function/code) and therefore optimize each occurance seperately. On the other hand, this could be premature optimization. It's up to you.

Categories

Resources