Skip to content

Commit fa6db2d

Browse files
hannaseithesteffenkleinle
authored andcommitted
2992 Add findMatchingSelection
1 parent c470e00 commit fa6db2d

File tree

6 files changed

+120
-8
lines changed

6 files changed

+120
-8
lines changed

native/src/components/CityEntry.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Text } from 'react-native'
33
import Highlighter from 'react-native-highlight-words'
44
import styled from 'styled-components/native'
55

6-
import { normalizeString } from 'shared'
6+
import { findBoldSections, normalizeString } from 'shared'
77
import { CityModel } from 'shared/api'
88

99
import { AppContext } from '../contexts/AppContextProvider'
@@ -65,6 +65,7 @@ const CityEntry = ({ city, query, navigateToDashboard }: CityEntryProps): ReactE
6565
textToHighlight={it}
6666
autoEscape
6767
sanitize={normalizeString}
68+
findChunks={findBoldSections}
6869
highlightStyle={{
6970
fontWeight: 'bold',
7071
}}
@@ -99,6 +100,7 @@ const CityEntry = ({ city, query, navigateToDashboard }: CityEntryProps): ReactE
99100
autoEscape
100101
textToHighlight={city.name}
101102
sanitize={normalizeString}
103+
findChunks={findBoldSections}
102104
highlightStyle={{
103105
fontWeight: 'bold',
104106
}}

shared/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export * from './utils/licences'
1313
export * from './utils/pois'
1414
export * from './utils/replaceLinks'
1515
export * from './utils/normalizeString'
16+
export * from './utils/findMatchingSections'
1617
export * from './utils'
1718
export * from './tracking'
1819
export * from './constants/maps'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { findMatchingSections } from '../findMatchingSections'
2+
3+
describe('findMatchingSections', () => {
4+
it('should find sections with ß', () => {
5+
expect(findMatchingSections({ searchWords: ['ß'], textToHighlight: 'aßaßaß' })).toEqual([
6+
{ start: 1, end: 2 },
7+
{ start: 3, end: 4 },
8+
{ start: 5, end: 6 },
9+
])
10+
expect(findMatchingSections({ searchWords: ['ß'], textToHighlight: 'wasserstraße' })).toEqual([
11+
{ start: 2, end: 4 },
12+
{ start: 10, end: 11 },
13+
])
14+
})
15+
it('should find sections with ss', () => {
16+
expect(findMatchingSections({ searchWords: ['ss'], textToHighlight: 'aßaßaß' })).toEqual([
17+
{ start: 1, end: 2 },
18+
{ start: 3, end: 4 },
19+
{ start: 5, end: 6 },
20+
])
21+
expect(findMatchingSections({ searchWords: ['ss'], textToHighlight: 'wasserstraße' })).toEqual([
22+
{ start: 2, end: 4 },
23+
{ start: 10, end: 11 },
24+
])
25+
})
26+
it('should find sections of words', () => {
27+
expect(findMatchingSections({ searchWords: ['straße'], textToHighlight: 'Strassenstraße' })).toEqual([
28+
{ start: 0, end: 7 },
29+
{ start: 8, end: 14 },
30+
])
31+
expect(findMatchingSections({ searchWords: ['städt'], textToHighlight: 'stadt städtchen' })).toEqual([
32+
{ start: 0, end: 5 },
33+
{ start: 6, end: 11 },
34+
])
35+
})
36+
})

shared/utils/findMatchingSections.ts

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import normalizeString from './normalizeString'
2+
3+
const mapReplacedString = (
4+
text: string,
5+
map: [string, string][],
6+
): { sanitizedText: string; replacementMap: number[] } => {
7+
let transformed = ''
8+
const repMap: number[] = []
9+
let i = 0
10+
let matcher
11+
while (i < text.length) {
12+
const findMatch = (index: number) => (element: [string, string]) => text.startsWith(element[0], index)
13+
14+
matcher = map.find(findMatch(i))
15+
if (matcher) {
16+
transformed += matcher[1]
17+
repMap.push(...Array(matcher[1].length).fill(i))
18+
i += matcher[0].length
19+
} else {
20+
transformed += text[i]
21+
repMap.push(i)
22+
i += 1
23+
}
24+
}
25+
return {
26+
sanitizedText: transformed,
27+
replacementMap: repMap,
28+
}
29+
}
30+
31+
export const findMatchingSections = ({
32+
searchWords,
33+
textToHighlight,
34+
}: {
35+
searchWords: (string | RegExp)[]
36+
textToHighlight: string
37+
}): { start: number; end: number }[] => {
38+
const { sanitizedText, replacementMap } = mapReplacedString(normalizeString(textToHighlight), [['ß', 'ss']])
39+
40+
let result: { start: number; end: number }[] = []
41+
if (replacementMap.length > 0) {
42+
searchWords.forEach((word: string | RegExp) => {
43+
let matches: { start: number; end: number }[] = []
44+
if (typeof word === 'string' && word !== '') {
45+
const sanitizedWord = normalizeString(word).replace('ß', 'ss')
46+
const regex = new RegExp(sanitizedWord, 'gi')
47+
48+
matches = [...sanitizedText.matchAll(regex)].map(match => {
49+
const start = replacementMap[match.index]
50+
const end = replacementMap[match.index + match[0].length] ?? textToHighlight.length
51+
if (start !== undefined && !Number.isNaN(start) && !Number.isNaN(end)) {
52+
return {
53+
start,
54+
end,
55+
}
56+
}
57+
return { start: 0, end: 0 }
58+
})
59+
}
60+
61+
if (matches.length > 0) {
62+
result = result.concat(matches)
63+
}
64+
})
65+
}
66+
67+
return result
68+
}
69+
70+
export default findMatchingSections

shared/utils/search.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
import CityModel from '../api/models/CityModel'
2-
import normalizeString from './normalizeString'
2+
import { normalizeString } from './normalizeString'
33

44
const cityFilter =
55
(filterText: string, developerFriendly: boolean) =>
66
(cityModel: CityModel): boolean => {
7-
const normalizedFilter = normalizeString(filterText)
8-
7+
const normalizedFilter = normalizeString(filterText).replace('ß', 'ss')
98
if (normalizedFilter === 'wirschaffendas') {
109
return !cityModel.live
1110
}
1211

1312
const validCity = cityModel.live || developerFriendly
1413
const aliases = Object.keys(cityModel.aliases ?? {})
15-
const matchesFilter = [cityModel.name, ...aliases].some(it => normalizeString(it).includes(normalizedFilter))
14+
const matchesFilter = [cityModel.name, ...aliases].some(it =>
15+
normalizeString(it).replace('ß', 'ss').includes(normalizedFilter),
16+
)
1617
return validCity && matchesFilter
1718
}
1819

web/src/components/CityEntry.tsx

+5-3
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import Highlighter from 'react-highlight-words'
33
import { Link } from 'react-router-dom'
44
import styled, { useTheme } from 'styled-components'
55

6-
import { cityContentPath, normalizeString } from 'shared'
6+
import { cityContentPath, normalizeString, findMatchingSections } from 'shared'
77
import { CityModel } from 'shared/api'
88

99
const MAX_NUMBER_OF_ALIASES = 3
@@ -35,10 +35,10 @@ type CityEntryProps = {
3535

3636
const CityEntry = ({ filterText, city, language }: CityEntryProps): ReactElement => {
3737
const theme = useTheme()
38-
const normalizedFilter = normalizeString(filterText)
38+
const normalizedFilter = normalizeString(filterText).replace('ß', 'ss')
3939
const aliases =
4040
city.aliases && normalizedFilter.length >= 1
41-
? Object.keys(city.aliases).filter(alias => normalizeString(alias).includes(normalizedFilter))
41+
? Object.keys(city.aliases).filter(alias => normalizeString(alias).replace('ß', 'ss').includes(normalizedFilter))
4242
: []
4343

4444
return (
@@ -48,6 +48,7 @@ const CityEntry = ({ filterText, city, language }: CityEntryProps): ReactElement
4848
sanitize={normalizeString}
4949
aria-label={city.name}
5050
textToHighlight={city.name}
51+
findChunks={findMatchingSections}
5152
autoEscape
5253
highlightStyle={{ backgroundColor: theme.colors.backgroundColor, fontWeight: 'bold' }}
5354
/>
@@ -59,6 +60,7 @@ const CityEntry = ({ filterText, city, language }: CityEntryProps): ReactElement
5960
searchWords={[filterText]}
6061
sanitize={normalizeString}
6162
textToHighlight={alias}
63+
findChunks={findMatchingSections}
6264
autoEscape
6365
highlightStyle={{ backgroundColor: theme.colors.backgroundColor, fontWeight: 'bold' }}
6466
/>

0 commit comments

Comments
 (0)