-
Notifications
You must be signed in to change notification settings - Fork 0
Functional programming
Fuctional programming is a style of programming where you create reusable functions that avoid side-effects. The function does the same thing every time. What is does only depends on what you give it and it will always give you the same result when you give it the same argument.
OOP is another programming style that creates reusable objects instead and includes methods that execute specific object related functionality instead. This has been my programming style for years now, but I am shifting now to a more functional programming style because it gives me more readable and reusable code. It is easier to repeat yourself with a OOP style while a more functional style challenges you to create elegant reusable code. I'm sure you can do this with OOP as well, but does not require you to do so, which makes it more forgiving than functional programming.
I have cleaned up the favourite books column from the survey.
To split a string you can use the split function which separates a string into an array on the passed argument. An normal split function looks like this:
const string = "a, b, c"
string.split(,)
// -> [a, b, c]
This line of code splits the string on every newline in the string. This is perfect if you want to split on a single character.
To split on multiple character you are going to need a regular expression (regex). Regex holds a pattern that checks if it can find a match in a string. Regex's will get quite complex rather easily, because you can use special characters to look for complex patterns.
Here is an example from MDN:
/\d+(?!\.)/
matches a number only if it is not followed by a decimal point. The regular expression/\d+> >>(?!\.)/.exec("3.141")
matches '141' but not '3.141'.
Things like this make regular expressions really powerful, but also hard to learn. I'm not an expert on the matter myself, but I can make simple regex's as I have also done to build a better split function.
In its simplest form a regex that looks for a newline looks like this /\n/g
. The patterns to look for are between the slashes(/). The g option afterwards means you are looking globally so through the entire string.
If you want to include more characters to your regex you should add a pipe (|). So if I also want to look for a comma (,) the regex looks like /\n|,/g
. I have used this idea to extend my split function. This is my version:
// splits a string using 1 or more separators
function splitString (string, ...separators) {
let regEx = new RegExp(separators.join("|"))
return string.split(regEx)
}
let surveyArray = utils.splitString(surveyString, "\n")
My function takes to parameters. It wants a string and some separators. I use to spread syntax because I don't need to know how many characters you pass to the function as arguments. It could be 0, 1 or 100. A spread syntax is always an array, regardless of the amount of passed arguments.
let surveyArray = utils.splitString(surveyString, "\n", ",", ";")
Next, I create a new regex using the separators. Because it is an array I have to use the join function to create a single string my regex can use. However, as previously mentioned a to look for multiple characters a pipe character needs to be in between the two characters. Therefore I add it in my join function to place it in between every value from the array.
A map function is almost the same as a foreach but returns another array instead of changing the original. This is perfect for functional programming, because you don't want side-effects to happen. Because I now have an array due to my split function, I can now use a map to do something with the results.
My function is defined like this:
function cleanArray (array, filterOptions, library) {
array.map (item => {
...
})
}
It takes 3 parameters, but the last one is optional. It wants an array to iterate over, filter options used to filter the array and a library to check for known titles.
The function basically contains to parts to handle to last 2 parameters. I recognise that this should be 2 separate functions, but for then sake of simplicity I've kept them in a single function.
if (library) {
library.forEach( titles => {
if (arrayIncludes(item, titles)) {
const preferredTitle = titles[titles.length - 1];
console.log(`[${item}] found in library, replace with preferred title [${preferredTitle}]`)
item = item.replace(item, preferredTitle)
}
})
}
A library contains all possible titles from a book and a preferred title. Because this is optional I first check if there is a library. I use a forEach to iterate over the library. Because I already have a map function. I don't need another array, so I can use foreach.
Next I check if the item is included in the library. I wrote a separate function to handle this.
function arrayIncludes (string, array){
return array.some( testString => string.includes(testString))
};
A library item looks like this:
[["game of thrones", "got", "A game of Thrones"], [...]]
The first strings are the possible title variants and the last string is always the preferred title.
The some() function tests whether a single item in the array passes a test. In this case I want to know if the string includes the testString which are the book titles variants in the library. This makes it less strict than using indexOf().
My first iteration of this functionality used indexOf.
function arrayIncludes (string, array){
return array.indexOf(string) > -1
}
indexOf returns -1 when it can't find the string and a positive number to indicate where the item is located in the array. By using > -1 I get true when it has found the string and false if I don't find it.
I wanted a less strict function that can 'recognise' a title by looking if the string contains a keyword(s) instead of having to type all variants of it. For example, my dataset contained multiple entries of The hunger games. Some typed it as hunger games, hunger games and some added the to it. This required me to add all variant in the library. When using includes() together with some() I just check if the string contains the keyword instead of having to type all variants of it.
The replace() function is quite explanatory. It replaces a (sub)string with something else.
item = item.replace(item, preferredTitle)
filterOptions.forEach(filter => {
const targetString = filter[0];
const newString = filter[1];
console.log(`[${targetString}] found in array, replace with [${newString}]`)
item = item.replace(targetString, newString)
})
Once again, I use a foreach here because I don't need another array. The filter contains the same structure as the library, but is more strict. Making it strict is important, because you want to explicitly define what you want to remove or replace. It's structured like this:
const filterOptions = ["/", ""][...],
The first item is what you want to remove and the second item is what you want to replace it with.
One thing I haven't mentioned is that I make everything lowercase before filtering. This is because filtering is case-sensitive. When I'm finished I make the first character uppercase.
item = item.charAt(0).toUpperCase() + item.slice(1);
charAt selects a character with then provided index. I make that character uppercase. Next, I add add the uppercased letter to the item, but removes the 2nd character, because that is actually the lowercase first character.
I wrote a separate module that included some handy utility functions
// capitalise first char from string
utilsController.capitaliseFirstCharacter = function(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
// --> "string" = "String"
This function capitalises the first character from a string. I used this in my query so when you add "kleding" it would also add "Kleding" in the query. It returns the string
// checks if string contains only numbers
utilsController.isStringANumber = function(string) {
return /^\d+$/.test(string);
}
// --> true/false
This function tests a string if it contains only numbers. It returns true or false. I used it to check whether a string is an amount or a name.
function getDataSet (uri) {
const url ="https://api.data.netwerkdigitaalerfgoed.nl/datasets/ivo/NMVW/services/NMVW-34/sparql";
const theSaurusUrl = `<https://hdl.handle.net/20.500.11840/${uri}>`;
const query = `
PREFIX dc: <http://purl.org/dc/elements/1.1/>
PREFIX dct: <http://purl.org/dc/terms/>
PREFIX skos: <http://www.w3.org/2004/02/skos/core#>
PREFIX edm: <http://www.europeana.eu/schemas/edm/>
PREFIX wgs84: <http://www.w3.org/2003/01/geo/wgs84_pos#>
PREFIX gn: <http://www.geonames.org/ontology#>
SELECT ?countryLabel
(COUNT(?cho) AS ?choCount)
WHERE {
${theSaurusUrl} skos:narrower* ?type .
?cho edm:object ?type .
?cho dct:spatial ?place . # obj place
?place skos:exactMatch/gn:parentCountry ?country .
?country gn:name ?countryLabel .
} GROUP BY ?country ?countryLabel
ORDER BY DESC(?choCount)
`
return runQuery(url, query);
}
```
#### Run the query
```
function runQuery(url, query) {
return fetch(url+"?query="+ encodeURIComponent(query) +"&format=json")
.then(res => res.json())
.then(json => {
return json.results.bindings;
});
}
```
This function executes a query on a url and returns the results.