URL Parse Exercise (JavaScript) - javascript

So here is a description of the problem that I've been talked to solve:
We need some logic that extracts the variable parts of a url into a hash. The keys
of the extract hash will be the "names" of the variable parts of a url, and the
values of the hash will be the values. We will be supplied with:
A url format string, which describes the format of a url. A url format string
can contain constant parts and variable parts, in any order, where "parts"
of a url are separated with "/". All variable parts begin with a colon. Here is
an example of such a url format string:
'/:version/api/:collection/:id'
A particular url instance that is guaranteed to have the format given by
the url format string. It may also contain url parameters. For example,
given the example url format string above, the url instance might be:
'/6/api/listings/3?sort=desc&limit=10'
Given this example url format string and url instance, the hash we want that
maps all the variable parts of the url instance to their values would look like this:
{
version: 6,
collection: 'listings',
id: 3,
sort: 'desc',
limit: 10
}
So I technically have a semi-working solution to this but, my questions are:
Am I understanding the task correctly? I'm not sure if I'm supposed to be dealing with two inputs (URL format string and URL instance) or if I'm just supposed to be working with one URL as a whole. (my solution takes two separate inputs)
In my solution, I keep reusing the split() method to chunk the array/s down and it feels a little repetitive. Is there a better way to do this?
If anyone can help me understand this challenge better and/or help me clean up my solution, it would be greatly appreciated!
Here is my JS:
const obj = {};
function parseUrl(str1, str2) {
const keyArr = [];
const valArr = [];
const splitStr1 = str1.split("/");
const splitStr2 = str2.split("?");
let val1 = splitStr2[0].split("/");
let val2 = splitStr2[1].split("&");
splitStr1.forEach((i) => {
keyArr.push(i);
});
val1.forEach((i) => {
valArr.push(i);
});
val2.forEach((i) => {
keyArr.push(i.split("=")[0]);
valArr.push(i.split("=")[1]);
});
for (let i = 0; i < keyArr.length; i++) {
if (keyArr[i] !== "" && valArr[i] !== "") {
obj[keyArr[i]] = valArr[i];
}
}
return obj;
};
console.log(parseUrl('/:version/api/:collection/:id', '/6/api/listings/3?sort=desc&limit=10'));
And here is a link to my codepen so you can see my output in the console:
https://codepen.io/TOOTCODER/pen/yLabpBo?editors=0012

Am I understanding the task correctly? I'm not sure if I'm supposed to
be dealing with two inputs (URL format string and URL instance) or if
I'm just supposed to be working with one URL as a whole. (my solution
takes two separate inputs)
Yes, your understanding of the problem seems correct to me. What this task seems to be asking you to do is implement a route parameter and a query string parser. These often come up when you want to extract data from part of the URL on the server-side (although you don't usually need to implement this logic your self). Do keep in mind though, you only want to get the path parameters if they have a : in front of them (currently you're retrieving all values for all), not all parameters (eg: api in your answer should be excluded from the object (ie: hash)).
In my solution, I keep reusing the split() method to chunk the array/s
down and it feels a little repetitive. Is there a better way to do
this?
The number of .split() methods that you have may seem like a lot, but each of them is serving its own purpose of extracting the data required. You can, however, change your code to make use of other array methods such as .map(), .filter() etc. to cut your code down a little. The below code also considers the case when no query string (ie: ?key=value) is provided:
function parseQuery(queryString) {
return queryString.split("&").map(qParam => qParam.split("="));
}
function parseUrl(str1, str2) {
const keys = str1.split("/")
.map((key, idx) => [key.replace(":", ""), idx, key.charAt(0) === ":"])
.filter(([,,keep]) => keep);
const [path, query = ""] = str2.split("?");
const pathParts = path.split("/");
const entries = keys.map(([key, idx]) => [key, pathParts[idx]]);
return Object.fromEntries(query ? [...entries, ...parseQuery(query)] : entries);
}
console.log(parseUrl('/:version/api/:collection/:id', '/6/api/listings/3?sort=desc&limit=10'));
It would be even better if you don't have to re-invent the wheel, and instead make use of the URL constructor, which will allow you to extract the required information from your URLs more easily, such as the search parameters, this, however, requires that both strings are valid URLs:
function parseUrl(str1, str2) {
const {pathname, searchParams} = new URL(str2);
const keys = new URL(str1).pathname.split("/")
.map((key, idx) => [key.replace(":", ""), idx, key.startsWith(":")])
.filter(([,,keep]) => keep);
const pathParts = pathname.split("/");
const entries = keys.map(([key, idx]) => [key, pathParts[idx]]);
return Object.fromEntries([...entries, ...searchParams]);
}
console.log(parseUrl('https://www.example.com/:version/api/:collection/:id', 'https://www.example.com/6/api/listings/3?sort=desc&limit=10'));
Above, we still need to write our own custom logic to obtain the URL parameters, however, we don't need to write any logic to extract the query string data as this is done for us by using URLSearchParams. We're also able to lower the number of .split()s used as we can obtain use the URL constructor to give us an object with a parsed URL already. If you end up using a library (such as express), you will get the above functionality out-of-the-box.

Related

Clean way to get value from string in Javascript

I have this string https://pokeapi.co/api/v2/pokemon/6/
I would like to extract the value after pokemon/ in this case 6. This represent Pokémon ids which could span between 1 -> N
I know this is pretty trivial and was wondering a nice solution for future proofing. Here is my solution.
const foo= "https://pokeapi.co/api/v2/pokemon/6/"
const result = foo.split('/') //[ 'https:', '', 'pokeapi.co', 'api', 'v2', 'pokemon', '6', '' ]
const ids = result[6]
You can grab the value after the last / character like so:
const pokemonID = foo.substring(foo.lastIndexOf("/") + 1)
Using String.lastIndexOf to get the final index of the slash character, and then using String.substring with only a single argument to parse the part of the string after that last / character. We add 1 to the lastIndexOf to omit the final slash.
For this to work you need to drop your final trailing slash (which won't do anything anyways) from your request URL.
This could be abstracted into a utility function to get the last value of any url, which is the biggest improvement over using a split and find by index approach.
However, beware, it will take whatever the value is after the last slash.
Using the string https://pokeapi.co/api/v2/pokemon/6/pokedex would return pokedex.
If you are using Angular, React, Vue etc with built in router, there will be specific APIs for the framework that can get the exact parameter you need regardless of URL shape.
You should use the built-in URL API to do the splitting correctly for you:
const url = new URL("https://pokeapi.co/api/v2/pokemon/6/");
Then you can get the pathname and split that:
const path = url.pathname.split("/");
After you split it you can get the value 6 by accessing the 5th element here:
const url = new URL("https://pokeapi.co/api/v2/pokemon/6/");
const path = url.pathname.split("/");
console.log(path[4]);
you could also do something like:
url.split('pokemon/')[1].split('/')[0]
Here is what I would do
const result = new URL(url).pathname.split('/');
const id = result[4];
I am not sure if this is better than yours
const foo= "https://pokeapi.co/api/v2/pokemon/6/"
const result = foo.indexOf("pokemon/");
const id_index = result + 8
const id = foo[id_index];

How to ignore invalid URL parameters?

Before I go on, let me say that I've looked through a number of threads already and can't find an answer that works for me.
Basically, I've built a custom link shortener and I'm using URLSearchParams to pull the URL to be shortened and the custom slug from the URL search query as follows:
var e = window.location.search;
const urlParams = new URLSearchParams(e);
const url = urlParams.get("url");
const slug = urlParams.get("slug");
Where the format for a query is: ?url=https://google.com&slug=customslug
After the parameters are handled, the URL string is treated with trim() to remove any whitespace. The final output is encoded with encodeURIComponent() when the API I'm using (https://short.io) is called.
However, I want to be able to pass URLs with &, like so: ?url=https://google.com/&testing&slug=customslug. My ideal solution would simply treat any & that isn't part of the &slug parameter as a part of the URL contained within the &url parameter. Currently, the & character is ignored if it isn't attached to a valid parameter (url or slug).
I have tried encoding the query input using encodeURIComponent(), but that results in a failure to pick up on either defined parameter. I have also tried splitting the input using split("&slug",1), but that results in an array and I cannot pass arrays to the Short.io API.
Any suggestions?
You should use the URL Encoded ampersand symbol %26.
var e = "?url=https://google.com/%26testing&slug=customslug";
const urlParams = new URLSearchParams(e);
const url = urlParams.get("url");
const slug = urlParams.get("slug");
console.log(url);
console.log(slug);
I solved my issue by building off of #CherryDT's comment about using window.location.hash to get my URL string. Ultimately, I chose to forgo the idea of a slug in the address bar, since it would cause further problems with my script.
While this solution is only applicable for my purposes, I'm detailing the solution because it functions as a workaround for the issue of not being able to encode a passed URL string from the address bar. Might be useful to someone, someday.
var e = window.location.href.replace(window.location.hash, '');
if (e.endsWith("?") === true) {
var url = window.location.hash.substr(1);
if (url === "") {
// Error code
} else {
console.log("A URL to be shortened was provided via hash.");
// Pass url to rest of script
}
}

Check if string matches

I've got a dataset, and each object has a promoUrl and a promoNumber, structured like so:
const phoneNumbers = [
{
promoUrl: '/interior-doors/',
promoNumber: '589-918-0710',
},
{
promoUrl: '/promo4/',
promoNumber: '307-789-8615',
},
];
On first load a cookie is set, containing the referral url (the const referrer in the code below) and the referral URL is passed to a reduce function allowing me to look for a URL, find the associated phone number, and then display that number dynamically.
const url = referrer;
const promoNumber = promoResults.reduce((promoNumber: string, results: any) => {
const hasPromo = url === results.promoUrl;
if (hasPromo) {
return results.promoNumber;
}
return promoNumber;
}, '');
However right now when it finds the URL it will only match the number if the cookie value matches the promoUrl exactly. Once this is live I won't be in charge of setting the promoUrls, and that task will go to non-developers. How do I set this so it works as long as the string contains matching characters, i.e. instead of needing /interior-doors/ it would work if the promoUrl is interior-doors or /interior-doors? Really anything so long as the string includes matching characters?
I've tried editing my hasPromo const using .includes():
const hasPromo = url.includes(results.promoUrl);
But it still hasn't worked for me.
have you log your url and results.promoUrl
suggestion is logs type and value of both keys ( url and promoUrl)
Figured this out after a bit of digging and some help in the comments from Robert Harvey.
All I need to do was modify the hasPromo const to check results.promoUrl first.
const hasPromo = results.promoUrl.includes(url);
This worked exactly like I need it to.

create one array after using map() twice

I may or may not get in 2 differently formatted bits of data.
They both need to be stripped of characters in different ways. Please excuse the variable names, I will make them better once I have this working.
const cut = flatten.map(obj => {
return obj.file.replace("0:/", "");
});
const removeDots = flatten.map(obj => {
return obj.file.replace("../../uploads/", "");
})
I then need to push the arrays into my mongo database.
let data;
for (const loop of cut) {
data = { name: loop };
product.images.push(data);
}
let moreData;
for (const looptwo of removeDots) {
moreData = {name: looptwo};
product.images.push(moreData);
}
I wanted to know if there is a way to either join them or do an if/else because the result of this is that if I have 2 records, it ends up duplicating and I get 4 records instead of 2. Also, 2 of the records are incorrectly formatted ie: the "0:/ is still present instead of being stripped away.
Ideally I would like have a check that if 0:/ is present, remove it, if ../../uploads/ is present or if both are present, remove both. And then create an array from that to push.
You can do your 2 replace on the same map :
const processed = flatten.map(obj => {
return obj.file.replace("0:/", "").replace("../../uploads/", "");
});
Since you know the possible patterns, you can create a regex and use it to replace any occurrences.
const regex = /(0:\/|(\.\.\/)+uploads\/)/g
const processed = flatten.map(obj => obj.file.replace(regex, ''));
You can verify here
Note, regex is a pattern based approach. So it has pros and cons.
Pro:
You can have any number of folder nesting. Using string ../../uploads/ will restrict you with 2 folder structure only.
You can achieve transformation in 1 operation and code looks clean.
Cons:
Regex can be hard to understand and can reduce readability of code a bit. (Opinionated)
If you have pattern like .../../uploads/bla, this will be parsed to .bla.
Since you ask also about a possible way of joining two arrays, I'll give you couple of solutions (with and w/o joining).
You can either chain .replace on the elements of the array, or you can concat the two arrays in your solution. So, either:
const filtered = flatten.map(obj => {
return obj.file.replace('0:/', '').replace('../../uploads/', '');
});
Or (joining the arrays):
// your two .map calls go here
const joinedArray = cut.concat(removeDots);

Split A string on a term AND THEN splitting on another term

I have a poorly designed URL query string that I can't easily change e.g.
https://mysite/.shtml?source=999&promotype=promo&cmpid=abc--dfg--hif-_-1234&cm=qrs-stv-_wyx&aff=45628_THIS+IS+Test_Example
I need to extract elements from it e.g. 45628
At the moment I'm using
document.URL.split(/aff=|_/)[5];
But I don't like this solution because if other parts of the URL structure change which is highly likely then my solution will break
Instead what I want to say is
split on "aff=" AND THEN split on "_"
Is there an easy way to do this, looking for a JS answer
Pretty sure you can do it like this:
document.URL.split("aff=")[1].split("_")[0];
I would start by splitting the string into tokens, if you can. Rather than working with foo=bar&fin=bin, break it down into [['foo', 'bar'], ['fin', 'bin]]. You can do that by splitting on the & and then the splitting each of those on the = character:
const data = 'source=999&promotype=promo&cmpid=abc--dfg--hif-_-1234&cm=qrs-stv-_wyx&aff=45628_THIS+IS+Test_Example';
console.log(data.split('&').map(it => it.split('=')));
Next, take the tokens you want and extract the leading digits:
const data = 'source=999&promotype=promo&cmpid=abc--dfg--hif-_-1234&cm=qrs-stv-_wyx&aff=45628_THIS+IS+Test_Example';
const tokens = data.split('&').map(it => it.split('='));
const [key,val] = tokens.find(([key]) => key === 'aff');
console.log(key, val.match(/[0-9]+/));
var url = 'https://mysite/.shtml?source=999&promotype=promo&cmpid=abc--dfg--hif-_-1234&cm=qrs-stv-_wyx&aff=45628_THIS+IS+Test_Example';
var re = new RegExp(/aff=(\d+)/);
var ext = re.exec(url)[1];
alert(ext)

Categories