forked from busbud/coding-challenge-backend-c
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsuggest.js
134 lines (127 loc) · 3.75 KB
/
suggest.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
/**
* Removes accents and diatrics of a string
* https://stackoverflow.com/questions/990904/remove-accents-diacritics-in-a-string-in-javascript
*
* @param {String} string
* @returns {String}
*/
const normalizeString = (string) =>
string
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, "")
.toLowerCase()
/**
* Returns a score between 1 and 0
*
* @param {Number} score
* @returns {Number}
*/
const normalizeScore = score => {
if (score > 1) return 1
if (score < 0) return 0
return score
}
/**
* Returns the distance in km between two points
* based on Haversine formula : https://stackoverflow.com/questions/27928/calculate-distance-between-two-latitude-longitude-points-haversine-formula
*
* @param {Number} lat1
* @param {Number} lon1
* @param {Number} lat2
* @param {Number} lon2
* @returns {Number}
*/
const getDistanceFromLatLonInKm = (lat1,lon1,lat2,lon2) => {
const deg2rad = (deg) => deg * (Math.PI/180)
const earthRadius = 6371; // in km
const dLat = deg2rad(lat2-lat1)
const dLon = deg2rad(lon2-lon1)
const a =
Math.sin(dLat/2) * Math.sin(dLat/2) +
Math.cos(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) *
Math.sin(dLon/2) * Math.sin(dLon/2)
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a))
const distance = earthRadius * c // Distance in km
return distance
}
/**
* Calculates a score based on string length
* more the city has a long name, smaller is the score
*
* @param {Object} city
* @param {String} normalizedSearch
* @returns {Number}
*/
const calculateScoreFromName = (city, normalizedSearch) => {
const score = 1 - (city.asciiname.length - normalizedSearch.length) * 0.1
return normalizeScore(score)
}
/**
* Calculates a score based on distance
* further is the city, smaller is the score
*
* @param {Number} lat1
* @param {Number} lon1
* @param {Numberany} lat2
* @param {Numberany} lon2
* @returns {Number}
*/
const calculateScoreFromDistance = (lat1,lon1,lat2,lon2) => {
// earth radius * 2 is roughly the max distance between two points
// for now we just make a simple linear fonction
const score = 1 - getDistanceFromLatLonInKm(lat1,lon1,lat2,lon2) / 6371 * 2
return normalizeScore(score)
}
/**
* Calculates the score of a city based on the search, the latitude and the longitude
* if latitude and longitude are given
* calculates a score based on the distance and the city name
* else
* calculates a score based on the city name
*
* @param {Object} city
* @param {String} normalizedSearch
* @param {Number} lat
* @param {Number} lon
* @returns {Number} a score between 0 and 1
*/
const calculateScore = (city, normalizedSearch, lat, lon) => {
let score
if (lat && lon) {
const nameWeight = 1
const distanceWeight = 1
score = (
calculateScoreFromName(city, normalizedSearch) * nameWeight
+ calculateScoreFromDistance(city.latitude, city.longitude, lat, lon) * distanceWeight
) / (nameWeight + distanceWeight)
} else {
score = calculateScoreFromName(city, normalizedSearch)
}
return normalizeScore(score)
}
/**
* Simple suggestion algorithm
*
* 1. it filters the city that has the beginging of its normalized name
* matching the normalized search
* 2. it adds a score to each result
* 3. it sorts by descending score
*
* @param {Object[]} cities
* @param {String} search
* @param {Number} lat
* @param {Number} lon
* @returns {Object[]}
*/
function suggest(cities, search, lat, lon) {
const normalizedSearch = normalizeString(search)
return cities
.filter(city =>
normalizeString(city.asciiname).indexOf(normalizedSearch) == 0
)
.map(city =>
Object.assign(city, {score: calculateScore(city, normalizedSearch, lat, lon)})
)
.sort((a,b) => b.score - a.score)
}
module.exports = suggest