I found myself having to process a string like:
foo=bar&foo1=foo%3Dbar%26foo2%3Dfoo%253Dbar
Into:
{
"foo": "bar",
"foo1": {
"foo": "bar",
"foo2": {
"foo": "bar"
}
}
}
A real input example.
My best attempt is:
function parse(input) {
try {
const parsed = JSON.parse(input);
return parseJSON(parsed);
} catch (err) {
const decodedInput = decodeURIComponent(input);
if (input.includes("&") && input.includes("=")) {
return input.split("&").reduce((json, part) => {
const [key, value] = part.split("=");
const decodedValue = decodeURIComponent(value);
return { ...json, [key]: parsePrimitive(decodedValue) };
}, {});
}
return decodedInput;
}
}
function parsePrimitive(input) {
if (!isNaN(input)) {
return Number(input);
}
if (input === "true" || input === "false") {
return input === "true";
}
return parse(input);
}
function parseJSON(input) {
return Object.entries(input).reduce((json, [key, value]) => {
let object = {};
if (typeof value === "object") {
if (Array.isArray(value)) {
object[key] = value;
} else {
object[key] = parseJSON(value);
}
} else {
const decodedValue = decodeURIComponent(value);
if (decodedValue.includes("&") && decodedValue.includes("=")) {
object[key] = parse(decodedValue);
} else {
object[key] = parsePrimitive(decodedValue);
}
}
return { ...json, ...object };
}, {});
}
If you try to run it, you're supposed to call parse(input)
However, it does fail for certain inputs
How can I make the perfect recursive algorithm for this kind of problem?
Thanks!
You could take a recursive approach by checking the encoded = sign.
const getValues = string => string.split('&')
.reduce((r, pair) => {
let [key, value] = pair.split('=');
value = decodeURIComponent(value);
r[key] = value.includes('=')
? getValues(value)
: value;
return r;
}, {});
console.log(getValues('foo=bar&foo1=foo%3Dbar%26foo2%3Dfoo%253Dbar'));
This seems to do it for your simple example and your more complex one (now updated to handle numbers and booleans):
const parse = (query) =>
query .startsWith ('{')
? JSON .parse (query)
: query .includes ('&') || query .includes ('=')
? Object .fromEntries (
query .split ('&')
.map (p => p .split ('='))
.map (([k, v]) => [k, parse (decodeURIComponent (v))])
)
: query .includes (',')
? query .split (',') .filter (Boolean) .map (parse)
: isFinite (query)
? Number (query)
: query .toLowerCase () == "true" || query .toLowerCase () == "false"
? query .toLowerCase () == "true"
: // else
query
const q = 'foo=bar&foo1=foo%3Dbar%26foo2%3Dfoo%253Dbar'
console .log (parse(q))
console.log('fetching larger example...')
fetch ('https://gist.githubusercontent.com/avi12/cd1d6728445608d64475809a8ddccc9c/raw/030974baed3eaadb26d9378979b83b1d30a265a3/url-input-example.txt')
.then (res => res .text ())
.then (parse)
.then (console .log)
.as-console-wrapper {max-height: 100% !important; top: 0}
There are two parts that deserve attention.
First, this makes an assumption about commas: that they represent a separation between elements of an array. And, further, it assumes that empty strings aren't intended, turning
watermark=%2Chttps%3A%2F%2Fs.ytimg.com%2Fyts%2Fimg%2Fwatermark%2Fyoutube_watermark-vflHX6b6E.png
%2Chttps%3A%2F%2Fs.ytimg.com%2Fyts%2Fimg%2Fwatermark%2Fyoutube_hd_watermark-vflAzLcD6.png
into this:
watermark: [
"https://s.ytimg.com/yts/img/watermark/youtube_watermark-vflHX6b6E.png",
"https://s.ytimg.com/yts/img/watermark/youtube_hd_watermark-vflAzLcD6.png"
]
The original starts with an encoded comma (%2C), which would lead to an initial empty string, so we use .filter (Boolean) to remove it.
Second, the test for a string representing JSON is very naïve, only doing .startsWith ('{'). You can replace this with whatever you need, but it leads to a question of intentions. I'm not sure we can write this entirely generically in this manner.
Still, I think it's close. And the code is fairly clean.
I do have to wonder why, however. This much data is going to run into various url size limits. At this point, wouldn't putting this into a request body rather than url parameters make much more sense?
Node.js comes with a built-in "querystring" npm package utility, but here I used a better one called "qs". You can specify delimiters in an array instead of only using one with the former.
If you want to use the built-in "querystring" package, you need to remove the delimiter array when calling parse and do a check on the string to see what delimiter is used - the sample file you gave use a few different ones.
So try this:
const qs = require("qs");
let params = `foo=bar&foo1=foo%3Dbar%26foo2%3Dfoo%253Dbar`;
const isObject = (param) => {
try {
let testProp = JSON.parse(param);
if (typeof testProp === "object" && testProp !== null) {
return true;
}
return false;
} catch (e) {
return false;
}
};
const isURL = (value) => {
try {
new URL(value);
} catch (e) {
return false;
}
return true;
};
const isQueryString = (value) => {
if (/[/&=]/.test(value) && !isURL(value)) {
return true;
} else {
return false;
}
};
const parseData = (data, parsed = false) => {
if (isQueryString(data) && !parsed) {
return parseData(qs.parse(data, { delimiter: /[;,/&]/ }), true);
} else if (isObject(data) || parsed) {
for (let propertyName in data) {
if (isObject(data[propertyName])) {
data[propertyName] = parseData(JSON.parse(data[propertyName]), true);
} else {
data[propertyName] = parseData(data[propertyName]);
}
}
return data;
} else {
return data;
}
};
let s = parseData(params);
console.log(JSON.stringify(s, null, 2));
I reworked the algorithm using Object.fromEntries(new URLSearchParams()).
function parse(query) {
try {
return JSON.parse(query);
} catch {
if (!isNaN(query)) {
return Number(query);
}
if (typeof query !== "string") {
const obj = {};
for (const queryKey in query) {
if (query.hasOwnProperty(queryKey)) {
obj[queryKey] = parse(query[queryKey]);
}
}
return obj;
}
if (!query) {
return "";
}
if (query.toLowerCase().match(/^(true|false)$/)) {
return query.toLowerCase() === "true";
}
const object = Object.fromEntries(new URLSearchParams(query));
const values = Object.values(object);
if (values.length === 1 && values[0] === "") {
return query;
}
return parse(object);
}
}
const q = 'foo=bar&foo1=foo%3Dbar%26foo2%3Dfoo%253Dbar';
console.log(parse(q));
console.log('fetching larger example...');
fetch('https://gist.githubusercontent.com/avi12/cd1d6728445608d64475809a8ddccc9c/raw/030974baed3eaadb26d9378979b83b1d30a265a3/url-input-example.txt')
.then(response => response.text())
.then(parse)
.then(console.log);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Related
I always use the else statement. And the data from the Data.js file. Is there something I might have overlooked?
import data from './Data'
function List(props){
const filteredData = data.filter((el) => {
if (props.input === '') {
return el
} else {
return el.text.tiLowerCase().includes(props.input)
}
})
That's just what the rule requires.
Disallows return before else.
If an if block contains a return statement, the else block becomes unnecessary. Its contents can be placed outside of the block.
If you think this rule is worth following, use instead (with correct spelling)
const filteredData = data.filter((el) => {
if (props.input === '') {
return el
}
return el.text.toLowerCase().includes(props.input)
})
or even better
const filteredData = data.filter((el) => {
return props.input === ''
? el
: el.text.toLowerCase().includes(props.input)
})
If el will alwys be truthy, then it would make more sense to return true instead.
const filteredData = data.filter((el) => {
return props.input === ''
? true
: el.text.toLowerCase().includes(props.input)
})
which allows for the simplification to
const filteredData = data.filter((el) => {
return props.input === '' || !el.text.toLowerCase().includes(props.input)
})
I want to encode a complex json/javascript object into the standard querystring encoding.
And i want to decode this querystring back to an json/javascript object.
It should be recursively, with arrays, objects, strings, booleans and numbers.
I thought this should be easy, but was proven wrong. Does anyone have an idea, how to solve this problem?
Either in Javascript or preferably in Typescript.
I think, what you want to do, is encode and decode nested objects.
There is no single standard, but very often, QS (Query String) syntax is used:
{
"attribute": "value",
"array": ["apples", "bananas"],
"object": { "number": 55 },
}
will become:
?attribute=value&array[0]=apples&array[1]=bananas&object[number]=55
Example code:
function decode(querystring: string): object {
function parseValue(value: string): any {
if (value === 'TRUE') return true;
if (value === 'FALSE') return false;
return isNaN(Number(value)) ? value : Number(value);
}
function dec(list: any[], isArray = false): object {
let obj: any = isArray ? [] : {};
let recs: any[] = list.filter((item) => {
if (item.keys.length > 1) return true;
obj[item.keys[0]] = parseValue(item.value);
});
let attrs = {};
recs.map((item) => {
item.key = item.keys.shift();
attrs[item.key] = [];
return item;
}).forEach((item) => attrs[item.key].push(item));
Object.keys(attrs).forEach((attr) => {
let nextKey = attrs[attr][0].keys[0];
obj[attr] = dec(attrs[attr], typeof nextKey === 'number');
});
return obj;
}
return dec(
querystring
.split('&')
.map((item) => item.split('=').map((x) => decodeURIComponent(x)))
.map((item) => {
return {
keys: item[0]
.split(/[\[\]]/g)
.filter((n) => n)
.map((key) => (isNaN(Number(key)) ? key : Number(key))),
value: item[1],
};
})
);
}
export function encode(object: object): string {
function reducer(obj, parentPrefix = null) {
return function (prev, key) {
const val = obj[key];
key = encodeURIComponent(key);
const prefix = parentPrefix ? `${parentPrefix}[${key}]` : key;
if (val == null || typeof val === 'function') {
prev.push(`${prefix}=`);
return prev;
}
if (typeof val === 'boolean') {
prev.push(`${prefix}=${val.toString().toUpperCase()}`);
return prev;
}
if (['number', 'string'].includes(typeof val)) {
prev.push(`${prefix}=${encodeURIComponent(val)}`);
return prev;
}
prev.push(
Object.keys(val).reduce(reducer(val, prefix), []).join('&')
);
return prev;
};
}
return Object.keys(object).reduce(reducer(object), []).join('&');
}
const obj = { key: 1, key1: 2, key2: 3 }
const params = encodeURIComponent(JSON.stringify(obj))
window.location.href = `http://example.com/?query=${params}`
end like this:
http://example.com/?query=%7B%22key%22%3A1%2C%22key1%22%3A2%2C%22key2%22%3A3%7D
const [fieldValues, updateFieldValues] = useState(fromJS({
imageName: '',
tags: [],
password: '',
}));
const handleButtonClick = () => {
const res = // logic to find which field/fields are empty
}
how can i validate if all the keys have value ?
p.s: i have the following requirements.
imageName and password should have at least one character.
tags should have at least one item.
You can use Yup a javascript library to vaidate values using a pre defined schema. Or else you can write a function that returns the keys of object that doesnt meet requirements
CUSTOM JS METHOD
function validateObj(obj) {
let isValid = true;
const errorKeys = [];
Object.entries(obj).forEach(([key, value]) =>{
if(!value) {
errorKeys.push(key)
isValid = false;
return;
}
if(Array.isArray(value)) {
if(!value?.length) {
errorKeys.push(key)
isValid = false;
return;
}
}
})
return { isValid, errorKeys }
}
IMMUTABLE JS METHOD
function validateObj(obj) {
let isValid = true;
const errorKeys = [];
console.log(obj)
obj.mapEntries(([key, value]) =>{
console.log(key, value)
if(!value) {
errorKeys.push(key)
isValid = false;
return;
}
if(List.isList(value)) {
if(!value.size) {
errorKeys.push(key)
isValid = false;
return;
}
}
})
return { isValid, errorKeys }
}
USAGE:
const handleButtonClick = () => {
const { isValid, errorKeys } = validateObj(obj);
if(!isValid) {
// SHOW ERROR MESSAGE HERE
}
}
Please feel free to reach out in case of any issues/ doubts
EDIT: Link to example fiddle
Immutable.js Collections are Iterables, so you can use for ... of and other common interactions on them. It also offers many ways of looking at the data, like forEach, find, findKey, findValue, filter. Take a look at the docs, they are good. A few examples can be seen below:
const thing = Immutable.fromJS({
banana: 'scale',
imageName: '',
tags: [],
password: '',
});
for([key, val] of thing) {
console.log('for of', key, ':', val);
}
thing.forEach((key, val) => {
console.log('forEach', key, ':', val);
});
const firstItemIdoNotLike = thing.findKey((key, val) => {
if(!!val) {
return true;
}
if (val.hasOwnProperty('length')) {
return val.length < 1;
}
return false;
});
if (firstItemIdoNotLike) {
console.log('validation error on', firstItemIdoNotLike);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/immutable/4.0.0-rc.12/immutable.js"></script>
I'm trying to parse a nested object of the format:
const obj = {
"and": [
{
"!=": [
{
"var": "name"
},
"name1"
]
},
{
"in": [
{
"var": "hobbies"
},
"jogging, video games"
]
}
]
};
I created a function to parse it:
const isOperatorCode(code) => {
const allowedOperatorCodes = ['in', '<=', '!=', '=='];
return allowedOperatorCodes.includes(code);
}
const parseObject = (arg, result={}) => {
if (arg === undefined || arg === null) return arg;
if (arg.constructor === Object && Object.keys(arg).length) {
for (const key in arg) {
const value = arg[key];
if (key === 'and' || key === 'or' || key === '!') {
result.operator = key === '!' ? 'not' : key;
result.operands = [];
return parseObject(value, result);
}
if (isOperatorCode(key)) {
const newItem = {
operator: key,
attribute: value[0].var,
value: value[1]
}
result.operands.push(newItem);
}
}
}
if(Array.isArray(arg) && arg.length) {
for (const k of arg) {
return parseObject(k, result);
}
}
return result;
}
I got this result when I executed the function:
{"operator":"and","operands":[{"operator":"!=","attribute":"name","value":"name1"}]}
it should be:
{"operator":"and","operands":[{"operator":"!=","attribute":"name","value":"name1"}, {"operator":"in","attribute":"hobbies","value":"sport, video games"}]}
I know that the array does not keep the trace of the elements to continue looping through the different items. Any idea or suggestions to keep the track of the array elements and loop on them all?
If you return only at the very-end, you will get your expected result.
const obj = {
"and": [{
"!=": [{
"var": "name"
}, "name1"]
}, {
"in": [{
"var": "hobbies"
}, "jogging, video games"]
}]
};
const isOperatorCode = (code) => {
const allowedOperatorCodes = ['in', '<=', '!=', '=='];
return allowedOperatorCodes.includes(code);
}
const parseObject = (arg, result = {}) => {
if (arg === undefined || arg === null) return arg;
if (arg.constructor === Object && Object.keys(arg).length) {
for (const key in arg) {
const value = arg[key];
if (key === 'and' || key === 'or' || key === '!') {
result.operator = key === '!' ? 'not' : key;
result.operands = [];
parseObject(value, result);
}
if (isOperatorCode(key)) {
const newItem = {
operator: key,
attribute: value[0].var,
value: value[1]
}
result.operands.push(newItem);
}
}
}
if (Array.isArray(arg) && arg.length) {
for (const k of arg) {
parseObject(k, result);
}
}
return result;
}
console.log(parseObject(obj));
.as-console-wrapper { top: 0; max-height: 100% !important; }
return parseObject(k, result); stops execution of the loop so you're only going to get the first item.
for (const k of arg) {
return parseObject(k, result); // return breaks out of the loop. Only processes the first item.
}
Perhaps this would make more sense?
return args.map(k => parseObject(k, result)); // process all entries, return an array.
You could take a recursive approach with look at objects with var as key.
const
convert = object => {
if (!object || typeof object !== 'object') return object;
const [operator, values] = Object.entries(object)[0];
return values[0] && typeof values[0] === 'object' && 'var' in values[0]
? { operator, attribute: values[0].var, value: values[1] }
: { operator, operands: values.map(convert) };
},
object = { and: [{ "!=": [{ var: "name" }, "name1"] }, { in: [{ var: "hobbies" }, "jogging, video games"] }] },
result = convert(object);
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
I am getting some api response , where I have to change all key value is camelcase ..
like below I am getting response . i have to convert all key is small .
like CBSServiceResponseDtls = cBSServiceResponseDtls same for all keys .
Dont want to change any of value And few of keys has _ .
Like "API_OUTPUT" I have to change in apiOutput.
Please help. Thanks
{
"CBSServiceResponseDtls":{
"Response":{
"ResultStatus":{
"ResultCode":0,
"ResultMessage":"SUCCESS"
},
"Data":{
"EVENT":{
"API_OUTPUT":{
"SUCCESS_FLAG":0,
"REQUEST_STATUS":0,
"ABILLITY_REF_NUM":32038,
"RESPONSE_ERROR_CODE":"",
"SUCCESS_MESG_LANG_1":"Request has been Processed Successfully",
"SUCCESS_MESG_LANG_2":"Request has been Processed Successfully",
"TRANSACTION_LOG_REFERNCE":2.0200507064507202e+27
},
"LOGIN_DETAILS":{
"LOGIN_DTLS":[
{
"LOGIN_NAME":"soban",
"LOCATION_CODE":"WAREHOUSE",
"LOCATION_CODE_NO":1,
"LOCATION_DESC":"WAREHOUSE",
"DEFAULT_FLAG":"Y"
},
{
"LOGIN_NAME":"soban",
"LOCATION_CODE":"BTP",
"LOCATION_CODE_NO":70,
"LOCATION_DESC":"BTP",
"DEFAULT_FLAG":"N"
}
]
}
}
}
}
}
}
Simple solution, You can use regex replace/transform function to change all keys.
More: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify
CamelCase Sample:
const toCamelCase = (str = "") => {
const [first, ...rest] = str.split("_");
return (
first.toLowerCase() +
rest
.map((word) => {
word = word.toLowerCase();
return word.slice(0, 1).toUpperCase() + word.slice(1);
})
.join("")
);
};
const tranform = (m) =>
m.indexOf("_") === -1
? m.slice(0, 1).toLowerCase() + m.slice(1)
: toCamelCase(m);
const formatJson = (obj) => {
const jsonString = JSON.stringify(obj).replace(
/\"(\w+)\":/g,
(_, m) => `"${tranform(m)}":`
);
return JSON.parse(jsonString);
};
const data = {"CBSServiceResponseDtls":{"Response":{"ResultStatus":{"ResultCode":0,"ResultMessage":"SUCCESS"},"Data":{"EVENT":{"API_OUTPUT":{"SUCCESS_FLAG":0,"REQUEST_STATUS":0,"ABILLITY_REF_NUM":32038,"RESPONSE_ERROR_CODE":"","SUCCESS_MESG_LANG_1":"Request has been Processed Successfully","SUCCESS_MESG_LANG_2":"Request has been Processed Successfully","TRANSACTION_LOG_REFERNCE":2.0200507064507202e+27},"LOGIN_DETAILS":{"LOGIN_DTLS":[{"LOGIN_NAME":"soban","LOCATION_CODE":"WAREHOUSE","LOCATION_CODE_NO":1,"LOCATION_DESC":"WAREHOUSE","DEFAULT_FLAG":"Y"},{"LOGIN_NAME":"soban","LOCATION_CODE":"BTP","LOCATION_CODE_NO":70,"LOCATION_DESC":"BTP","DEFAULT_FLAG":"N"}]}}}}}}
const json = formatJson(data);
console.log("%j", json);
You can use this helper function to do it recursively:
function objToLowerCase(obj) {
if (Array.isArray(obj)) {
return obj.map((entry) => typeof entry !== "object" ? entry : objToLowerCase(entry));
}
const newObj = Object.entries(obj).reduce((obj, [key, value]) => {
const newKey = key.toLowerCase().split("_").join("");
const newValue = typeof value !== "object" ? value : objToLowerCase(value);
obj[newKey] = newValue;
return obj;
}, {});
return newObj;
}
Here is a sandbox.
To make it camelCase, you could use this function:
function toCamelCase(str) {
if (str === str.toUpperCase()) {
return str
.toLowerCase()
.split("_")
.map((s, i) =>
i === 0 ? s : s.slice(0, 1).toUpperCase() + s.slice(1, s.length)
)
.join("");
} else {
return str.slice(0, 1).toLowerCase() + str.slice(1);
}
}
and call it like this:
function objToLowerCase(obj) {
if (Array.isArray(obj)) {
return obj.map((entry) => typeof entry !== "object" ? entry : objToLowerCase(entry));
}
const newObj = Object.entries(obj).reduce((obj, [key, value]) => {
const newKey = toCamelCase(key)
const newValue = typeof value !== "object" ? value : objToLowerCase(value);
obj[newKey] = newValue;
return obj;
}, {});
return newObj;
}