Cleaning up multiple variables in Javascript - javascript

I would like to clean up my variable declaration and am wondering if there is a better way to go about it.
Below I have a function that is creating chart data based on the length of an array. I am declaring each array before I am adding the values to them. I have 4 different areas (pa1-4) and 6 different statuses. I declare a variable for each status for each area, as well as a variable for the total.
function jobcharts(data) {
//declaring each variable
var i;
var pa1Sum = [],
pa1SumComplete = [],
pa1SumGenSM = [],
pa1SumQue = [],
pa1SumInP = [],
pa1SumFin = [],
pa1SumOH = [],
pa2Sum = [],
pa2SumComplete = [],
pa2SumGenSM = [],
pa2SumQue = [],
pa2SumInP = [],
pa2SumFin = [],
pa2SumOH = [](...etc);
//going through all data
for (i = 0; i < data.features.length; i++) {
var jobArea = data.features[i].properties.JOB;
if (data.features[i].properties.PRIORITY_AREA === "PA1") {
pa1Sum.push(data.features[i]);
if (data.features[i].properties["JOB STATUS"] === "ON HOLD") {
pa1SumOH.push(data.features[i].properties.JOB);
} else if (data.features[i].properties["JOB STATUS"] === "Generating SM") {
pa1SumGenSM.push(data.features[i].properties.JOB);
} else if (data.features[i].properties["JOB STATUS"] === "Queued") {
pa1SumQue.push(data.features[i].properties.JOB);
} else if (data.features[i].properties["JOB STATUS"] === "In Progress") {
pa1SumInP.push(data.features[i].properties.JOB);
} else if (data.features[i].properties["JOB STATUS"] === "Final Review") {
pa1SumFin.push(data.features[i].properties.JOB);
} else {
pa1SumComplete.push(data.features[i].properties.JOB);
} (...etc);
I then get the length after the loop finishes and structure the data as needed.
function arraytest(array, total) {
if (array.length > 0) {
return (Math.round(((array.length) / total.length) * 100));
} else {
return "0";
}
}
var pa1Arr = [arraytest(pa1SumComplete, pa1Sum), arraytest(pa1SumFin, pa1Sum), arraytest(pa1SumInP, pa1Sum), arraytest(pa1SumQue, pa1Sum), arraytest(pa1SumGenSM, pa1Sum), arraytest(pa1SumOH, pa1Sum)];
Is there a better way to structure the variables so I do not have 24 declarations at the beginning of my function?

Yup, just take the job status and group by that:
const totals = {};
let count = 0;
for(const { properties: { ["JOB STATUS"]: status, JOB: job, PRIORITY_AREA: priority } } of data.features) {
if(priority === "Pa1") {
count++;
if(!totals[status])
totals[status] = [];
totals[status].push(job);
}
}
To now get the percentages one could map the objects values:
Object.values(totals).map(entries => entries.length / count * 100);

Related

JavaScript Need help writing shouldQuarantine prototype

Need this to determine if any of the passengers are isHealthy =false then quarantine the wagon. I may have an issue on the join prototype as well. The isHealthy is only triggered if they eat and have no food. So it is possible for them to eat, and then have no food but not trigger isHealthy.
I am very new to this please be patient.
const Traveler = function (travelerName) {
this.name = travelerName;
this.food = 1;
this.isHealthy = true;
};
Traveler.prototype.hunt = function () {
this.food += 2;
console.log(this.food);
};
Traveler.prototype.eat = function () {
this.food -= 1;
if (this.food === 1) {
} else {
this.food === 0;
this.isHealthy = false;
}
console.log(this.food);
};
console.log(new Traveler("John"));
function Wagon(capacity) {
this.capacity = capacity;
this.passengers = [];
}
console.log(new Wagon(4));
Wagon.prototype.getAvailableSeatCount = function () {
let seatingCapacity = this.capacity - this.passengers.length;
console.log(seatingCapacity);
return seatingCapacity;
};
Wagon.prototype.join = function (traveler) {
console.log(this.capacity);
let currentCapacity = this.capacity;
if (currentCapacity <= this.passengers.length) {
this.currentCapacity = 0;
} else if (this.getAvailableSeatCount != 0) {
this.passengers.push(traveler);
}
console.log(this.passengers);
};
Wagon.prototype.shouldQuarantine = function () {
for (let i = 0; i < this.passengers.length; i++) {
if (this.passengers[i].isHealthy) {
return false;
}
}
};
Wagon.prototype.totalFood = function () {
let totalFood = "";
this.passengers.forEach(this.food);
console.log(this.food);
};
In you eat method of the Traveler class, first check if there is any food. If there is, then subtract one and check if the food is now empty. If it is then set isHealthy to false.
Traveler.prototype.eat = function () {
if (this.food > 0) {
this.food -= 1;
if (this.food === 0) {
this.isHealthy = false;
}
}
};
Subsequently you should also modify your hunt method to make your traveler healthy again after hunting.
Traveler.prototype.hunt = function () {
this.food += 2;
this.isHealthy = true;
};
In the shouldQuarantine method of the Wagon class, instead of checking if all passengers are healthy, check if anyone of them is not healthy and return true if that is the case.
If everyone is healthy, the loop will finish. Return false after the loop.
Wagon.prototype.shouldQuarantine = function () {
for (let i = 0; i < this.passengers.length; i++) {
if (!this.passengers[i].isHealthy) {
return true;
}
}
return false;
};
Alternatively you could use the some method on the this.passengers array to check if any of the passenger isn't healthy.
Wagon.prototype.shouldQuarantine = function () {
return this.passengers.some(
passenger => passenger.isHealthy === false
);
};
The join method can be simplyfied. You only need the result from this.getAvailableSeatCount() to see if there is any room. Add the traveler if the result is not 0.
Wagon.prototype.join = function (traveler) {
const availableSeats = this.getAvailableSeatCount();
if (availableSeats !== 0) {
this.passengers.push(traveler);
}
};
I also noticed that the totalFood method doesn't work as expected, but I'll let this one up to you. Hint: totalFood should be a number. Loop over every passenger and add the amount of food to the totalFood value.

Getting Cannot set properties of undefined

I have function CarFactory, which produce cars and should change color of these cars.
const warehouse = require("./warehouse");
function CarFactory(power = 10) {
this.warehouse = warehouse;
this.produceCar = (color = "red", wheels = 4, engine = false) => {
if (power < 2) {
return null
} else {
let car = { "id": warehouse.nextIdentifier, "color": color, "wheels": wheels, "engine": engine }
warehouse.createdCars.push(car);
warehouse.nextIdentifier++;
}
}
this.addEnergyPower = (value = 0) => {
power += value;
}
this.changeCarColor = (num) => {
if (power < 1) {
return null
} else {
warehouse.createdCars[num].color = 'blue'
}
}
}
module.exports = CarFactory;
But Im getting error Cannot set properties of undefined(setting 'color').
If I hardcode 0 like this to: createdCars[0] it actually works for car indexed 0.
this is warehouse file
let warehouse = {
createdCars: [],
nextIdentifier: 0
};
module.exports = warehouse;
this is where jest tries to change color
for (let i = 0; i < myFactory.warehouse.createdCars.length; i += 2) {
let car = myFactory.warehouse.createdCars[i];
if (myFactory.changeCarColor(car) !== null) {} else {
if (energyBoosts.length > 0) {
myFactory.addEnergyPower(energyBoosts.shift());
i -= 2;
} else {
break
}
}
}
From what I can tell in the line if (myFactory.changeCarColor(car) !== null) {} else{ car would be { "id": warehouse.nextIdentifier, "color": color, "wheels": wheels, "engine": engine } but what it seems like you would want is the id. So change if (myFactory.changeCarColor(car) !== null) {} else{ to if (myFactory.changeCarColor(car.id) !== null) {} else{ and see if that works.
You have this.changeCarColor = (num) but appears you want to pass a car to that and then get the id? I have no idea what this is so I then fails here:
console.log(typeof energyBoosts); with "undefined" for energyBoosts
But that is the NEXT question not the one at hand.
let warehouse = {
createdCars: [],
nextIdentifier: 0
};
function CarFactory(power = 10) {
this.warehouse = warehouse;
this.produceCar = (color = "red", wheels = 4, engine = false) => {
// console.log("here");
if (power < 2) {
return null;
} else {
let car = {
"id": warehouse.nextIdentifier,
"color": color,
"wheels": wheels,
"engine": engine
};
warehouse.createdCars.push(car);
warehouse.nextIdentifier++;
}
}
this.addEnergyPower = (value = 0) => {
power += value;
}
this.changeCarColor = (car) => {
console.log(car.color, car.id, power);
if (power < 1) {
return null;
} else {
warehouse.createdCars[car.id].color = 'blue';
}
}
}
let myFactory = new CarFactory();
//console.log(myFactory);
for (let c = 0; c < 10; c++) {
//console.log(c);
myFactory.produceCar("red", 4, false);
}
//console.log("GO!", myFactory.warehouse);
for (let i = 1; i < myFactory.warehouse.createdCars.length; i += 2) {
//console.log(i);
let car = myFactory.warehouse.createdCars[i];
console.log("Car:", car);
if (myFactory.changeCarColor(car) == null) {
console.log(typeof energyBoosts);
if (energyBoosts.length > 0) {
myFactory.addEnergyPower(energyBoosts.shift());
i -= 2;
} else {
break;
}
}
}

JavaScript array has elements but length is zero

I've done some searching around the web and nothing seems to solve my problem. I have the following jQuery code:
function youtube_data_parser(data) {
//---> parse video data - start
var qsToJson = function(qs) {
var res = {};
var pars = qs.split('&');
var kv, k, v;
for (i in pars) {
kv = pars[i].split('=');
k = kv[0];
v = kv[1];
res[k] = decodeURIComponent(v);
}
return res;
}
//---> parse video data - end
var get_video_info = qsToJson(data);
if (get_video_info.status == 'fail') {
return {
status: "error",
code: "invalid_url",
msg: "check your url or video id"
};
} else {
// remapping urls into an array of objects
//--->parse > url_encoded_fmt_stream_map > start
//will get the video urls
var tmp = get_video_info["url_encoded_fmt_stream_map"];
if (tmp) {
tmp = tmp.split(',');
for (i in tmp) {
tmp[i] = qsToJson(tmp[i]);
}
get_video_info["url_encoded_fmt_stream_map"] = tmp;
}
//--->parse > url_encoded_fmt_stream_map > end
//--->parse > player_response > start
var tmp1 = get_video_info["player_response"];
if (tmp1) {
get_video_info["player_response"] = JSON.parse(tmp1);
}
//--->parse > player_response > end
//--->parse > keywords > start
var keywords = get_video_info["keywords"];
if (keywords) {
key_words = keywords.replace(/\+/g, ' ').split(',');
for (i in key_words) {
keywords[i] = qsToJson(key_words[i]);
}
get_video_info["keywords"] = {
all: keywords.replace(/\+/g, ' '),
arr: key_words
};
}
//--->parse > keywords > end
//return data
return {
status: 'success',
raw_data: qsToJson(data),
video_info: get_video_info
};
}
}
function getVideoInfo() {
var get_video_url = $('#ytdlUrl').val();
var get_video_id = getUrlVars(get_video_url)['v'];
var video_arr_final = [];
var ajax_url = "video_info.php?id=" + get_video_id;
$.get(ajax_url, function(d1) {
var data = youtube_data_parser(d1);
var video_data = data.video_info;
var player_info = data.video_info.player_response;
var video_title = player_info.videoDetails.title.replace(/\+/g, ' ');
var fmt_list = video_data.fmt_list.split(',');
var video_thumbnail_url = video_data.thumbnail_url;
var video_arr = video_data.url_encoded_fmt_stream_map;
//create video file array
$.each(video_arr, function(i1, v1) {
var valueToPush = {};
valueToPush.video_url = v1.url;
valueToPush.video_thumbnail_url = video_thumbnail_url;
valueToPush.video_title = video_title;
$.each(fmt_list, function(i2, v2) {
var fmt = v2.split('/');
var fmt_id = fmt[0];
var fmt_quality = fmt[1];
if (fmt_id == v1.itag) {
valueToPush.fmt_id = fmt_id;
valueToPush.fmt_quality = fmt_quality;
}
});
video_arr_final.push(valueToPush);
});
});
return video_arr_final;
}
function getUrlVars(url) {
var vars = {};
var parts = url.replace(/[?&]+([^=&]+)=([^&]*)/gi, function(m, key, value) {
vars[key] = value;
});
return vars;
}
function fillInOptions(ytOptions) {
//console.log(ytOptions);
//alert(ytOptions[0]);
var ytFill = ytOptions;
console.log(ytFill);
//ytFill.forEach(function(i,v) {
var ytdlOptions = $('#ytdlOptions');
ytFill.forEach(function(i,v) {
console.log(i);
ytdlOptions.append(new Option(v.fmt_quality, v.fmt_id));
});
return true;
}
function showYTDLLoader() {
$('#ytdlInput').fadeOut(1000, function() {
$('#ytdlLoader').fadeIn(500);
});
var options = getVideoInfo();
//console.log(options);
if (fillInOptions(options) == true) {
//do rest
}
}
function showYTDLOptions() {
return true;
}
function startDownload() {
showYTDLLoader();
}
function hideYTDLLoader() {
$('#ytdlLoader').fadeOut(500);
}
function animateCSS(element, animationName, callback) {
const node = $(element);
node.addClass(animationName);
function handleAnimationEnd() {
node.removeClass(animationName);
node.animationend = null;
if (typeof callback === 'function') callback();
}
node.animationend = handleAnimationEnd();
}
When my button is clicked, I call showYTDLLoader() which gets an array of objects from the YouTube API that looks like this:
[
{
"video_url": "https://r7---sn-uxanug5-cox6.googlevideo.com/videoplayback?expire=1572496003&ei=Iw66Xa24H8PL3LUPiN25mAs&ip=2001%3A8003%3A749b%3Aa01%3A5cd8%3Ac610%3A6402%3Ad0fe&id=o-ADsVnoOoBQ6-SWzYZU7gHES06s7xQptJG6hn9WcakITY&itag=22&source=youtube&requiressl=yes&mm=31%2C29&mn=sn-uxanug5-cox6%2Csn-ntqe6n7r&ms=au%2Crdu&mv=m&mvi=6&pl=39&initcwndbps=1655000&mime=video%2Fmp4&ratebypass=yes&dur=917.768&lmt=1572418007364260&mt=1572474311&fvip=4&fexp=23842630&c=WEB&txp=5535432&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cmime%2Cratebypass%2Cdur%2Clmt&sig=ALgxI2wwRgIhAIp-4gyUTLoXFetbY0ha_YnR7DJqsp_MNjjIxqDdfPZJAiEA_WPd21jgX9broBcigf8rcSEVoJb2_NX7t3XZQqytsSM%3D&lsparams=mm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AHylml4wRAIgacvP3zjEq-rVEZFrX7a_hC6TR-Zab7Ii-Fbaupjs_PcCIHdZht4l4ioYL3ERz7WNiSbnOnhm5iYxEECaQXPP2hUp",
"video_title": "Arnold Schwarzenegger on Son-in-law Chris Pratt, Pranking Sylvester Stallone & Terminator’s Return",
"fmt_id": "22",
"fmt_quality": "1280x720"
},
{
"video_url": "https://r7---sn-uxanug5-cox6.googlevideo.com/videoplayback?expire=1572496003&ei=Iw66Xa24H8PL3LUPiN25mAs&ip=2001%3A8003%3A749b%3Aa01%3A5cd8%3Ac610%3A6402%3Ad0fe&id=o-ADsVnoOoBQ6-SWzYZU7gHES06s7xQptJG6hn9WcakITY&itag=18&source=youtube&requiressl=yes&mm=31%2C29&mn=sn-uxanug5-cox6%2Csn-ntqe6n7r&ms=au%2Crdu&mv=m&mvi=6&pl=39&initcwndbps=1655000&mime=video%2Fmp4&gir=yes&clen=44248820&ratebypass=yes&dur=917.768&lmt=1572416976690256&mt=1572474311&fvip=4&fexp=23842630&c=WEB&txp=5531432&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cmime%2Cgir%2Cclen%2Cratebypass%2Cdur%2Clmt&sig=ALgxI2wwRQIhANTZJlBHFWQWCnfK11yvLiPUV26c6NzvqIMKjDwmsByMAiBUSy0ZJMo4GdHSiRU4xBDDLxLtzwKZAqAKCiB-1aViDQ%3D%3D&lsparams=mm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AHylml4wRAIgacvP3zjEq-rVEZFrX7a_hC6TR-Zab7Ii-Fbaupjs_PcCIHdZht4l4ioYL3ERz7WNiSbnOnhm5iYxEECaQXPP2hUp",
"video_title": "Arnold Schwarzenegger on Son-in-law Chris Pratt, Pranking Sylvester Stallone & Terminator’s Return",
"fmt_id": "18",
"fmt_quality": "640x360"
}
]
But when I try and loop through each entry with fillInOptions(), my loop is never completed because the length is apparently zero. However, when I dump the array using console.log() it tells me the length is 2, and displays the above. I need to be able to add each option to my dropdown.
Thankyou!
UPDATE: Added full code, sorry!
It looks like your .forEach() is the root of the problem. The parameters of a forEach are currentValue, index like this: array.forEach(function(currentValue, index) {}); but it looks like you're using them in the opposite way
Try rewriting that iteration to this:
ytFill.forEach(function(v, i) {
console.log(i);
ytdlOptions.append(new Option(v.fmt_quality, v.fmt_id));
});
Notice the difference in the order of v and i in the parameters.

Building an AST in JavaScript

I am attempting to build a parser for BBCode in JavaScript that will allow me to transpile a string with BBCode in it to a string with HTML. I have in my head how it is all supposed to work and I even have two of the parser steps built.
Right now the entire process of the parser can be described as
Get input
Break input into tokens (tokenize)
Add information about the tokens (lex)
Build the AST from the tokens (parse)
Clean up the AST based on grammar rules (clean)
Evaluate AST and transform to HTML (evaluate)
Return the HTML string
I have a general idea of how to do all of this in my head except for step four.
When I reached step four I ran into a problem when building the AST. The problem was how would I go about recursively building this tree. I have in the past recursively built two dimensional arrays but a variable depth tree is way out of my scope of abilities.
In my head I think that the tree should look something like this:
// Hello, [b]World![/b]
{
"text": "Hello, ",
"tag": {
"type": "b",
"text": "World!"
}
}
But when trying to generate this I have an issue with recursively building this down.
A more complex example would be as follows:
// [c=red]Hello Tom, [/c][b][c=green]how are you?[/c][/b]
{
"tag": {
type: "c",
"parameters": "red",
"text": "Hello Tom, "
"tag": {
"type": "b",
"tag": {
"type": "c",
"parameters": "green",
"text": "how are you?"
}
}
}
}
The main issue I run across is keeping my place while building down without accidentally overwriting the entire tree.
Currently the code I am using is:
var bbcode = {};
bbcode._tokens = {
'TO_DEL': '[',
'TC_DEL': ']',
'TE_DEL': '/',
'EQ_DEL': '='
};
bbcode._tags = ['c', 'b'];
bbcode.parse = function(bbcode) {
var tokens = this._tokenize(bbcode);
tokens = this._lex(tokens);
var ast = this._parse(tokens);
console.log(JSON.stringify(ast, null, 4));
//return tokens;
};
bbcode._isToken = function(token) {
for (var k in this._tokens) {
if (this._tokens[k] === token) {
return true;
}
}
return false;
};
bbcode._isTag = function(token) {
return (this._tags.indexOf(token) > -1) ? true : false;
};
bbcode._getType = function(token) {
for (var k in this._tokens) {
if (this._tokens[k] === token) {
return k;
}
}
};
bbcode._next = function(tokens, curr) {
return tokens[curr + 1][0];
};
bbcode._previous = function(tokens, curr) {
return tokens[curr - 1][0];
};
bbcode._tokenize = function(bbcode) {
var tree = [];
var temp = '';
for (var i = 0; i < bbcode.length; i++) {
if (this._isToken(bbcode[i])) {
if (temp.length > 0) {
tree.push(temp);
temp = '';
}
tree.push(bbcode[i]);
} else {
temp += bbcode[i];
}
}
return tree;
};
bbcode._lex = function(tokens) {
var tree = [];
for (var i = 0; i < tokens.length; i++) {
if (this._isToken(tokens[i])) {
tree.push([this._getType(tokens[i]), tokens[i]]);
} else if (this._isTag(tokens[i])) {
tree.push(['BB_TAG', tokens[i]]);
} else {
tree.push(['BB_STRING', tokens[i]]);
}
}
return tree;
};
/*****************************************************************************/
/* I need help with the block below */
/*****************************************************************************/
bbcode._parse = function(tokens) {
var tree = {};
for (var i = 0; i < tokens.length; i++) {
if (tokens[i][0] === 'BB_STRING') {
if (tree['text']) {
tree['text'] += tokens[i][1];
} else {
tree['text'] = tokens[i][1];
}
} else if (tokens[i][0] === 'TO_DEL') {
if (this._next(tokens, i) === 'BB_TAG') {
tree['tag'] = {};
} else {
if (tree['text']) {
tree['text'] += tokens[i][1];
} else {
tree['text'] = tokens[i][1];
}
}
}
}
return tree;
};
/*****************************************************************************/

Javascript: Determine unknown array length and map dynamically

Going to do my best at explaining what I am trying to do.
I have two models, mine and an api response I am receiving. When the items api response comes in, I need to map it to my model and inserts all the items. This is simple of course. Heres the issue, I need to do so without really knowing what I am dealing with. My code will be passed in two strings, one of my models mapping path and one of the api response mapping path.
Here are the two paths
var myPath = "outputModel.items[].uniqueName"
var apiPath = "items[].name"
Basically FOR all items in apiPath, push into items in myPath and set to uniqueName
What it comes down to is that my code has NO idea when two items need to be mapped, or even if they contain an array or simple field to field paths. They could even contain multiple arrays, like this:
******************** EXAMPLE *************************
var items = [
{
name: "Hammer",
skus:[
{num:"12345qwert"}
]
},
{
name: "Bike",
skus:[
{num:"asdfghhj"},
{num:"zxcvbn"}
]
},
{
name: "Fork",
skus:[
{num:"0987dfgh"}
]
}
]
var outputModel = {
storeName: "",
items: [
{
name: "",
sku:""
}
]
};
outputModel.items[].name = items[].name;
outputModel.items[].sku = items[].skus[].num;
************************ Here is the expected result of above
var result = {
storeName: "",
items: [
{
name: "Hammer",
sku:"12345qwert"
},
{
name: "Bike",
sku:"asdfghhj"
},
{
name: "Bike",
sku:"zxcvbn"
},
{
name: "Fork",
sku:"0987dfgh" }
]
};
I will be given a set of paths for EACH value to be mapped. In the case above, I was handed two sets of paths because I am mapping two values. It would have to traverse both sets of arrays to create the single array in my model.
Question - How can I dynamically detect arrays and move the data around properly no matter what the two model paths look like? Possible?
So you have defined a little language to define some data addressing and manipulation rules. Let's think about an approach which will allow you to say
access(apiPath, function(value) { insert(myPath, value); }
The access function finds all the required items in apiPath, then calls back to insert, which inserts them into myPath. Our job is to write functions which create the access and insert functions; or, you could say, "compile" your little language into functions we can execute.
We will write "compilers" called make_accessor and make_inserter, as follows:
function make_accessor(program) {
return function(obj, callback) {
return function do_segment(obj, segments) {
var start = segments.shift() // Get first segment
var pieces = start.match(/(\w+)(\[\])?/); // Get name and [] pieces
var property = pieces[1];
var isArray = pieces[2]; // [] on end
obj = obj[property]; // drill down
if (!segments.length) { // last segment; callback
if (isArray) {
return obj.forEach(callback);
} else {
return callback(obj);
}
} else { // more segments; recurse
if (isArray) { // array--loop over elts
obj.forEach(function(elt) { do_segment(elt, segments.slice()); });
} else {
do_segment(obj, segments.slice()); // scalar--continue
}
}
}(obj, program.split('.'));
};
}
We can now make an accessor by calling make_accessor('items[].name').
Next, let's write the inserter:
function make_inserter(program) {
return function(obj, value) {
return function do_segment(obj, segments) {
var start = segments.shift() // Get first segment
var pieces = start.match(/(\w+)(\[\])?/); // Get name and [] pieces
var property = pieces[1];
var isArray = pieces[2]; // [] on end
if (segments.length) { // more segments
if (!obj[property]) {
obj[property] = isArray ? [] : {};
}
do_segment(obj, segments.slice());
} else { // last segment
obj[property] = value;
}
}(obj, program.split('.'));
};
}
Now, you can express your whole logic as
access = make_accessor('items[].name');
insert = make_inserter('outputModel.items[].uniqueName');
access(apiPath, function(val) { insert(myPath, val); });
As mentioned in the comments, there is no strict definition of the input format, it is hard to do it with perfect error handling and handle all corner cases.
Here is my lengthy implementation that works on your sample, but might fail for some other cases:
function merge_objects(a, b) {
var c = {}, attr;
for (attr in a) { c[attr] = a[attr]; }
for (attr in b) { c[attr] = b[attr]; }
return c;
}
var id = {
inner: null,
name: "id",
repr: "id",
type: "map",
exec: function (input) { return input; }
};
// set output field
function f(outp, mapper) {
mapper = typeof mapper !== "undefined" ? mapper : id;
var repr = "f("+outp+","+mapper.repr+")";
var name = "f("+outp;
return {
inner: mapper,
name: name,
repr: repr,
type: "map",
clone: function(mapper) { return f(outp, mapper); },
exec:
function (input) {
var out = {};
out[outp] = mapper.exec(input);
return out;
}
};
}
// set input field
function p(inp, mapper) {
var repr = "p("+inp+","+mapper.repr+")";
var name = "p("+inp;
return {
inner: mapper,
name: name,
repr: repr,
type: mapper.type,
clone: function(mapper) { return p(inp, mapper); },
exec: function (input) {
return mapper.exec(input[inp]);
}
};
}
// process array
function arr(mapper) {
var repr = "arr("+mapper.repr+")";
return {
inner: mapper,
name: "arr",
repr: repr,
type: mapper.type,
clone: function(mapper) { return arr(mapper); },
exec: function (input) {
var out = [];
for (var i=0; i<input.length; i++) {
out.push(mapper.exec(input[i]));
}
return out;
}
};
}
function combine(m1, m2) {
var type = (m1.type == "flatmap" || m2.type == "flatmap") ? "flatmap" : "map";
var repr = "combine("+m1.repr+","+m2.repr+")";
return {
inner: null,
repr: repr,
type: type,
name: "combine",
exec:
function (input) {
var out1 = m1.exec(input);
var out2 = m2.exec(input);
var out, i, j;
if (m1.type == "flatmap" && m2.type == "flatmap") {
out = [];
for (i=0; i<out1.length; i++) {
for (j=0; j<out2.length; j++) {
out.push(merge_objects(out1[i], out2[j]));
}
}
return out;
}
if (m1.type == "flatmap" && m2.type != "flatmap") {
out = [];
for (i=0; i<out1.length; i++) {
out.push(merge_objects(out1[i], out2));
}
return out;
}
if (m1.type != "flatmap" && m2.type == "flatmap") {
out = [];
for (i=0; i<out2.length; i++) {
out.push(merge_objects(out2[i], out1));
}
return out;
}
return merge_objects(out1, out2);
}
};
}
function flatmap(mapper) {
var repr = "flatmap("+mapper.repr+")";
return {
inner: mapper,
repr: repr,
type: "flatmap",
name: "flatmap",
clone: function(mapper) { return flatmap(mapper); },
exec:
function (input) {
var out = [];
for (var i=0; i<input.length; i++) {
out.push(mapper.exec(input[i]));
}
return out;
}
};
}
function split(s, t) {
var i = s.indexOf(t);
if (i == -1) return null;
else {
return [s.slice(0, i), s.slice(i+2, s.length)];
}
}
function compile_one(inr, outr) {
inr = (inr.charAt(0) == ".") ? inr.slice(1, inr.length) : inr;
outr = (outr.charAt(0) == ".") ? outr.slice(1, outr.length) : outr;
var box = split(inr, "[]");
var box2 = split(outr, "[]");
var m, ps, fs, i, j;
if (box == null && box2 == null) { // no array!
m = id;
ps = inr.split(".");
fs = outr.split(".");
for (i=0; i<fs.length; i++) { m = f(fs[i], m); }
for (j=0; j<ps.length; j++) { m = p(ps[j], m); }
return m;
}
if (box != null && box2 != null) { // array on both sides
m = arr(compile_one(box[1], box2[1]));
ps = box[0].split(".");
fs = box[0].split(".");
for (i=0; i<fs.length; i++) { m = f(fs[i], m); }
for (j=0; j<ps.length; j++) { m = p(ps[j], m); }
return m;
}
if (box != null && box2 == null) { // flatmap
m = flatmap(compile_one(box[1], outr));
ps = box[0].split(".");
for (j=0; j<ps.length; j++) { m = p(ps[j], m); }
return m;
}
return null;
}
function merge_rules(m1, m2) {
if (m1 == null) return m2;
if (m2 == null) return m1;
if (m1.name == m2.name && m1.inner != null) {
return m1.clone(merge_rules(m1.inner, m2.inner));
} else {
return combine(m1, m2);
}
}
var input = {
store: "myStore",
items: [
{name: "Hammer", skus:[{num:"12345qwert"}]},
{name: "Bike", skus:[{num:"asdfghhj"}, {num:"zxcvbn"}]},
{name: "Fork", skus:[{num:"0987dfgh"}]}
]
};
var m1 = compile_one("items[].name", "items[].name");
var m2 = compile_one("items[].skus[].num", "items[].sku");
var m3 = compile_one("store", "storeName");
var m4 = merge_rules(m3,merge_rules(m1, m2));
var out = m4.exec(input);
alert(JSON.stringify(out));
I have borrowed earlier answer and made improvements so as to solve both your examples and this should be generic. Though if you plan to run this sequencially with 2 sets of inputs, then the behavior will be as I have outlined in my comments to your original question.
var apiObj = {
items: [{
name: "Hammer",
skus: [{
num: "12345qwert"
}]
}, {
name: "Bike",
skus: [{
num: "asdfghhj"
}, {
num: "zxcvbn"
}]
}, {
name: "Fork",
skus: [{
num: "0987dfgh"
}]
}]
};
var myObj = { //Previously has values
storeName: "",
items: [{
uniqueName: ""
}],
outputModel: {
items: [{
name: "Hammer"
}]
}
};
/** Also works with this **
var myPath = "outputModel.items[].uniqueName";
var apiPath = "items[].name";
*/
var myPath = "outputModel.items[].sku";
var apiPath = "items[].skus[].num";
function make_accessor(program) {
return function (obj, callback) {
(function do_segment(obj, segments) {
var start = segments.shift() // Get first segment
var pieces = start.match(/(\w+)(\[\])?/); // Get name and [] pieces
var property = pieces[1];
var isArray = pieces[2]; // [] on end
obj = obj[property]; // drill down
if (!segments.length) { // last segment; callback
if (isArray) {
return obj.forEach(callback);
} else {
return callback(obj);
}
} else { // more segments; recurse
if (isArray) { // array--loop over elts
obj.forEach(function (elt) {
do_segment(elt, segments.slice());
});
} else {
do_segment(obj, segments.slice()); // scalar--continue
}
}
})(obj, program.split('.'));
};
}
function make_inserter(program) {
return function (obj, value) {
(function do_segment(obj, segments) {
var start = segments.shift() // Get first segment
var pieces = start.match(/(\w+)(\[\])?/); // Get name and [] pieces
var property = pieces[1];
var isArray = pieces[2]; // [] on end
if (segments.length) { // more segments
if (!obj[property]) {
obj[property] = isArray ? [] : {};
}
do_segment(obj[property], segments.slice());
} else { // last segment
if (Array.isArray(obj)) {
var addedInFor = false;
for (var i = 0; i < obj.length; i++) {
if (!(property in obj[i])) {
obj[i][property] = value;
addedInFor = true;
break;
}
}
if (!addedInFor) {
var entry = {};
entry[property] = value;
obj.push(entry);
}
} else obj[property] = value;
}
})(obj, program.split('.'));
};
}
access = make_accessor(apiPath);
insert = make_inserter(myPath);
access(apiObj, function (val) {
insert(myObj, val);
});
console.log(myObj);
(old solution: https://jsfiddle.net/d7by0ywy/):
Here is my new generalized solution when you know the two objects to process in advance (called inp and out here). If you don't know them in advance you can use the trick in the old solution to assign the objects on both sides of = to inp and out (https://jsfiddle.net/uxdney3L/3/).
Restrictions: There has to be the same amount of arrays on both sides and an array has to contain objects. Othewise it would be ambiguous, you would have to come up with a better grammar to express rules (or why don't you have functions instead of rules?) if you want it to be more sophisticated.
Example of ambiguity: out.items[].sku=inp[].skus[].num Do you assign an array of the values of num to sku or do you assign an array of objects with the num property?
Data:
rules = [
'out.items[].name=inp[].name',
'out.items[].sku[].num=inp[].skus[].num'
];
inp = [{
'name': 'Hammer',
'skus':[{'num':'12345qwert','test':'ignore'}]
},{
'name': 'Bike',
'skus':[{'num':'asdfghhj'},{'num':'zxcvbn'}]
},{
'name': 'Fork',
'skus':[{'num':'0987dfgh'}]
}];
Program:
function process() {
if (typeof out == 'undefined') {
out = {};
}
var j, r;
for (j = 0; j < rules.length; j++) {
r = rules[j].split('=');
if (r.length != 2) {
console.log('invalid rule: symbol "=" is expected exactly once');
} else if (r[0].substr(0, 3) != 'out' || r[1].substr(0, 3) != 'inp') {
console.log('invalid rule: expected "inp...=out..."');
} else {
processRule(r[0].substr(3).split('[]'), r[1].substr(3).split('[]'), 0, inp, out);
}
}
}
function processRule(l, r, n, i, o) { // left, right, index, in, out
var t = r[n].split('.');
for (var j = 0; j < t.length; j++) {
if (t[j] != '') {
i = i[t[j]];
}
}
t = l[n].split('.');
if (n < l.length - 1) {
for (j = 0; j < t.length - 1; j++) {
if (t[j] != '') {
if (typeof o[t[j]] == 'undefined') {
o[t[j]] = {};
}
o = o[t[j]];
}
}
if (typeof o[t[j]] == 'undefined') {
o[t[j]] = [];
}
o = o[t[j]];
for (j = 0; j < i.length; j++) {
if (typeof o[j] == 'undefined') {
o[j] = {};
}
processRule(l, r, n + 1, i[j], o[j]);
}
} else {
for (j = 0; j < t.length - 1; j++) {
if (t[j] != '') {
if (typeof o[t[j]] == 'undefined') {
o[t[j]] = {};
}
o = o[t[j]];
}
}
o[t[j]] = i;
}
}
process();
console.log(out);
Well, an interesting problem. Programmatically constructing nested objects from a property accessor string (or the reverse) isn't much of a problem, even doing so with multiple descriptors in parallel. Where it does get complicated are arrays, which require iteration; and that isn't as funny any more when it gets to different levels on setter and getter sides and multiple descriptor strings in parallel.
So first we need to distinguish the array levels of each accessor description in the script, and parse the text:
function parse(script) {
return script.split(/\s*[;\r\n]+\s*/g).map(function(line) {
var assignment = line.split(/\s*=\s*/);
return assignment.length == 2 ? assignment : null; // console.warn ???
}).filter(Boolean).map(function(as) {
as = as.map(function(accessor) {
var parts = accessor.split("[]").map(function(part) {
return part.split(".");
});
for (var i=1; i<parts.length; i++) {
// assert(parts[i][0] == "")
var prev = parts[i-1][parts[i-1].length-1];
parts[i][0] = prev.replace(/s$/, ""); // singular :-)
}
return parts;
});
if (as[0].length == 1 && as[1].length > 1) // getter contains array but setter does not
as[0].unshift(["output"]); // implicitly return array (but better throw an error)
return {setter:as[0], getter:as[1]};
});
}
With that, the textual input can be made into a usable data structure, and now looks like this:
[{"setter":[["outputModel","items"],["item","name"]],
"getter":[["items"],["item","name"]]},
{"setter":[["outputModel","items"],["item","sku"]],
"getter":[["items"],["item","skus"],["sku","num"]]}]
The getters already transform nicely into nested loops like
for (item of items)
for (sku of item.skus)
… sku.num …;
and that's exactly where we are going to. Each of those rules is relatively easy to process, copying properties on objects and iterating array for array, but here comes our most crucial issue: We have multiple rules. The basic solution when we deal with iterating multiple arrays is to create their cartesian product and this is indeed what we will need. However, we want to restrict this a lot - instead of creating every combination of all names and all nums in the input, we want to group them by the item that they come from.
To do so, we'll build some kind of prefix tree for our output structure that'll contain generators of objects, each of those recursivley being a tree for the respective output substructure again.
function multiGroupBy(arr, by) {
return arr.reduce(function(res, x) {
var p = by(x);
(res[p] || (res[p] = [])).push(x);
return res;
}, {});
}
function group(rules) {
var paths = multiGroupBy(rules, function(rule) {
return rule.setter[0].slice(1).join(".");
});
var res = [];
for (var path in paths) {
var pathrules = paths[path],
array = [];
for (var i=0; i<pathrules.length; i++) {
var rule = pathrules[i];
var comb = 1 + rule.getter.length - rule.setter.length;
if (rule.setter.length > 1) // its an array
array.push({
generator: rule.getter.slice(0, comb),
next: {
setter: rule.setter.slice(1),
getter: rule.getter.slice(comb)
}
})
else if (rule.getter.length == 1 && i==0)
res.push({
set: rule.setter[0],
get: rule.getter[0]
});
else
console.error("invalid:", rule);
}
if (array.length)
res.push({
set: pathrules[0].setter[0],
cross: product(array)
});
}
return res;
}
function product(pathsetters) {
var groups = multiGroupBy(pathsetters, function(pathsetter) {
return pathsetter.generator[0].slice(1).join(".");
});
var res = [];
for (var genstart in groups) {
var creators = groups[genstart],
nexts = [],
nests = [];
for (var i=0; i<creators.length; i++) {
if (creators[i].generator.length == 1)
nexts.push(creators[i].next);
else
nests.push({path:creators[i].path, generator: creators[i].generator.slice(1), next:creators[i].next});
}
res.push({
get: creators[0].generator[0],
cross: group(nexts).concat(product(nests))
});
}
return res;
}
Now, our ruleset group(parse(script)) looks like this:
[{
"set": ["outputModel","items"],
"cross": [{
"get": ["items"],
"cross": [{
"set": ["item","name"],
"get": ["item","name"]
}, {
"get": ["item","skus"],
"cross": [{
"set": ["item","sku"],
"get": ["sku","num"]
}]
}]
}]
}]
and that is a structure we can actually work with, as it now clearly conveys the intention on how to match together all those nested arrays and the objects within them.
Let's dynamically interpret this, building an output for a given input:
function transform(structure, input, output) {
for (var i=0; i<structure.length; i++) {
output = assign(output, structure[i].set.slice(1), getValue(structure[i], input));
}
return output;
}
function retrieve(val, props) {
return props.reduce(function(o, p) { return o[p]; }, val);
}
function assign(obj, props, val) {
if (!obj)
if (!props.length) return val;
else obj = {};
for (var j=0, o=obj; j<props.length-1 && o!=null && o[props[j]]; o=o[props[j++]]);
obj[props[j]] = props.slice(j+1).reduceRight(function(val, p) {
var o = {};
o[p] = val;
return o;
}, val);
return obj;
}
function getValue(descriptor, input) {
if (descriptor.get) // && !cross
return retrieve(input, descriptor.get.slice(1));
var arr = [];
descriptor.cross.reduce(function horror(next, d) {
if (descriptor.set)
return function (inp, cb) {
next(inp, function(res){
cb(assign(res, d.set.slice(1), getValue(d, inp)));
});
};
else // its a crosser
return function(inp, cb) {
var g = retrieve(inp, d.get.slice(1)),
e = d.cross.reduce(horror, next)
for (var i=0; i<g.length; i++)
e(g[i], cb);
};
}, function innermost(inp, cb) {
cb(); // start to create an item
})(input, function(res) {
arr.push(res); // store the item
});
return arr;
}
And this does indeed work with
var result = transform(group(parse(script)), items); // your expected result
But we can do better, and much more performant:
function compile(structure) {
function make(descriptor) {
if (descriptor.get)
return {inputName: descriptor.get[0], output: descriptor.get.join(".") };
var outputName = descriptor.set[descriptor.set.length-1];
var loops = descriptor.cross.reduce(function horror(next, descriptor) {
if (descriptor.set)
return function(it, cb) {
return next(it, function(res){
res.push(descriptor)
return cb(res);
});
};
else // its a crosser
return function(it, cb) {
var arrName = descriptor.get[descriptor.get.length-1],
itName = String.fromCharCode(it);
var inner = descriptor.cross.reduce(horror, next)(it+1, cb);
return {
inputName: descriptor.get[0],
statement: (descriptor.get.length>1 ? "var "+arrName+" = "+descriptor.get.join(".")+";\n" : "")+
"for (var "+itName+" = 0; "+itName+" < "+arrName+".length; "+itName+"++) {\n"+
"var "+inner.inputName+" = "+arrName+"["+itName+"];\n"+
inner.statement+
"}\n"
};
};
}, function(_, cb) {
return cb([]);
})(105, function(res) {
var item = joinSetters(res);
return {
inputName: item.inputName,
statement: (item.statement||"")+outputName+".push("+item.output+");\n"
};
});
return {
statement: "var "+outputName+" = [];\n"+loops.statement,
output: outputName,
inputName: loops.inputName
};
}
function joinSetters(descriptors) {
if (descriptors.length == 1 && descriptors[0].set.length == 1)
return make(descriptors[0]);
var paths = multiGroupBy(descriptors, function(d){ return d.set[1] || console.error("multiple assignments on "+d.set[0], d); });
var statements = [],
inputName;
var props = Object.keys(paths).map(function(p) {
var d = joinSetters(paths[p].map(function(d) {
var names = d.set.slice(1);
names[0] = d.set[0]+"_"+names[0];
return {set:names, get:d.get, cross:d.cross};
}));
inputName = d.inputName;
if (d.statement)
statements.push(d.statement)
return JSON.stringify(p) + ": " + d.output;
});
return {
inputName: inputName,
statement: statements.join(""),
output: "{"+props.join(",")+"}"
};
}
var code = joinSetters(structure);
return new Function(code.inputName, code.statement+"return "+code.output+";");
}
So here is what you will get in the end:
> var example = compile(group(parse("outputModel.items[].name = items[].name;outputModel.items[].sku = items[].skus[].num;")))
function(items) {
var outputModel_items = [];
for (var i = 0; i < items.length; i++) {
var item = items[i];
var skus = item.skus;
for (var j = 0; j < skus.length; j++) {
var sku = skus[j];
outputModel_items.push({"name": item.name,"sku": sku.num});
}
}
return {"items": outputModel_items};
}
> var flatten = compile(group(parse("as[]=bss[][]")))
function(bss) {
var as = [];
for (var i = 0; i < bss.length; i++) {
var bs = bss[i];
for (var j = 0; j < bs.length; j++) {
var b = bs[j];
as.push(b);
}
}
return as;
}
> var parallelRecords = compile(group(parse("x.as[]=y[].a; x.bs[]=y[].b")))
function(y) {
var x_as = [];
for (var i = 0; i < y.length; i++) {
var y = y[i];
x_as.push(y.a);
}
var x_bs = [];
for (var i = 0; i < y.length; i++) {
var y = y[i];
x_bs.push(y.b);
}
return {"as": x_as,"bs": x_bs};
}
And now you can easily pass your input data to that dynamically created function and it will be transformed quite fast :-)

Categories

Resources