Multiple word search using trie in dart - flutter
I'm trying to implement trie search in flutter. And here's the entire trie.dart file.
The idea is something like this, say we have I have a list of recipe names:
Burger
French Fries
Ice Cream
Garlic Parmesan Butter
Now I need to search using prefix so if the user searches for bur it'll show Burger. But if someone write Garlic Butter I need to return Garlic Parmesan Butter. So, basically if the search query has multiple words I need to show the correct name.
Here's the part where I get all words with prefix:
List<String> getAllWordsWithPrefix(String prefix) {
StringBuffer fullPrefix = new StringBuffer();
return _getAllWordsWithPrefixHelper(prefix, _head, fullPrefix);
}
List<String> _getAllWordsWithPrefixHelper(
String prefix, _TrieNode node, StringBuffer fullPrefix) {
if (prefix.length == 0) {
String pre = fullPrefix.toString();
return _collect(
new StringBuffer(pre.substring(0, max(pre.length - 1, 0))), node, []);
}
for (_TrieNode child in node.children) {
if ((child.char == prefix.substring(0, 1)) ||
(!_isCaseSensitive &&
child.char.substring(0, child.char.length).toLowerCase() ==
prefix.substring(0, 1).toLowerCase())) {
fullPrefix.write(child.char);
return _getAllWordsWithPrefixHelper(
prefix.substring(1), child, fullPrefix);
}
}
return [];
}
And finally I'm using the trie search in the following way(thought this might help someway):
class Search {
final Map<String, RecipeModel> _map = Map.fromIterable(
Store.instance.getAllRecipes(),
// ignore: non_constant_identifier_names
// key: (recipe) => RecipeModel().recipeName!,
key: (recipe) => recipe.recipeName!,
);
late final Trie trie;
Search() {
// This will be O[n]
trie = Trie.list(_map.keys.toList());
}
late RecipeModel recipe;
RecipeModel returnRecipe(String? suggestion) {
if (suggestion == null) return recipe;
// This will be O(1) instead of O(n) [better]
final RecipeModel? found = _map[suggestion];
return found ?? recipe;
}
List<String> returnSuggestions(String prefix) {
//will return O[W*L] ad-hoc search was O[n^2]
return trie.getAllWordsWithPrefix(prefix);
}
}
First, your TrieNode is using a List, which means you have to do a for-loop O(N) search for each char. It would be faster to use a Map.
(You could also use a 26D array, instead of a map, if you know that you'll only have English letters: [0] = 'A', [1] = 'B', ... [25] = 'Z')
Part I: garlic butter
Now I need to search using prefix so if the user searches for bur it'll show Burger. But if someone write Garlic Butter I need to Garlic Parmesan Butter. So, basically if the search query has multiple words I need to show the correct name.
bur is easy to do, as that's what a Trie data structure is for, but the garlic butter one is harder. You probably want to look into how to implement a fuzzy search or "did you mean...?" type algorithm:
Fuzzy search algorithm (approximate string matching algorithm)
However, maybe this is more complex than you want.
Here are some alternatives I thought of:
Option #1
Change your TrieNode to store a String variable stating the "standard" word. So the final TrieNode of garlic parmesan butter and garlic butter would both have an instance variable that stores garlic butter (the main standard/common word you want to use).
Your TrieNode would be like this:
class _TrieNode {
//...other vars...
String _standardWord;
}
Then your add method would look like this:
String standardWord = 'garlic butter';
trie.addWord('garlic butter', standardWord);
trie.addWord('garlic parmesan butter', standardWord);
Internally, your add method would set the standardWord of the last char's TrieNode to the 2nd param of addWord.
Internally, your find method would return the standardWord, which was set in the last char's TrieNode.
Then you only have to test for garlic butter and don't have to worry about parmesan.
Does that make sense?
Of course, you can also flip this around so that it always returns garlic parmesan butter instead:
String standardWord = 'garlic parmesan butter';
trie.addWord('garlic butter', standardWord);
trie.addWord('garlic parmesan butter', standardWord);
This requires that you know all phrases that it could be in advance, and then you add them to your Trie, with all common phrases pointing to the same standard word.
Option #2
Split your phrase/sentence into words, based on the spaces between words.
So your Trie would look like this:
trie.addWord('garlic');
trie.addWord('parmesan');
trie.addWord('butter');
When you have all of the matching words, you then need to write an algorithm (logic) to piece them together in a meaningful way.
Here's an example:
Set<String> wordsFromTrie = {};
List<String> words = "garlic parmesan butter".split(RegExp(r'\s+'));
words.forEach((word) => wordsFromTrie.add(trie.findWord(word)));
if(wordsFromTrie.contains("garlic") && wordsFromTrie.contains("butter")) {
print("Garlic parmesan butter!");
}
Option #3
Use some type of Regex matching instead of a Trie, so for example, your Regex would be RegExp(r"garlic.*butter",caseSensitive: false) and this would match all garlic butter regardless of what's in the middle.
This will be a bit slower, as you'll have a bunch of if-statements with Regexes. You could make it faster by first testing someString.startsWith('garlic') (after stripping spaces and lower-casing).
Option #4
Use a combination of #1 and #3.
This would mean you'd have a special RegexTrieNode that you would add.
trie.addWord('garlic',RegExp(r'[^b]+'),'butter');
When you hit RegexTrieNode it would continue to match each char until it stops matching. When it stops matching, you would need to go to the next child, butter, for the rest of the matching.
It's quite complicated, and it won't work for all Regexes, but it's doable.
Part II: bur => burger
Basically, once you reach the end of bur, you need to keep going down (or up?) the Trie for TrieNodes that only have 1 child.
Why only 1 child? Well, if you have burrito and burger, which does bur match? It's ambiguous.
Here's some example code, roughly based on your code. It uses null-safety. If you don't have that enabled, then remove all ? from the code.
import 'dart:math';
void main() {
var trie = Trie();
trie.addWord("burger");
// All results will be "burger".
print(trie.findWord("b"));
print(trie.findWord("bur"));
print(trie.findWord("burger"));
// This will be null, but fixable
// if want to allow longer strings.
print(trie.findWord("burgerme"));
}
class Trie {
TrieNode head = TrieNode();
void addWord(String? word) {
if(word == null || word.isEmpty) {
return;
}
var currentNode = head;
// Rune is a unicode codepoint (unicode char).
word.runes.forEach((rune) {
var childNode = currentNode.children[rune];
if(childNode == null) {
childNode = TrieNode();
currentNode.children[rune] = childNode; // Add to parent.
}
currentNode = childNode; // Next node.
});
// Last node is the last char.
currentNode.endOfWord = true;
}
String? findWord(String partial) {
var word = StringBuffer();
var currentNode = head;
partial.runes.forEach((rune) {
var childNode = currentNode.children[rune];
if(childNode == null) {
return null; // Not found.
}
word.writeCharCode(rune);
currentNode = childNode; // Next node.
});
// Prevent "burgerme" from matching to "burger".
// Uncomment this if-block if want to allow it.
if(currentNode.endOfWord && partial.length > word.length) {
return null;
}
// This logic allows "bur" to match to "burger".
while(!currentNode.endOfWord) {
// Ambiguous: "bur" could match either "burger" or "burrito".
if(currentNode.children.length != 1) {
return null; // Don't know.
}
var onlyChild = currentNode.children.entries.first;
word.writeCharCode(onlyChild.key);
currentNode = onlyChild.value; // Next node.
}
return word.toString();
}
}
class TrieNode {
Map<int,TrieNode> children = {};
bool endOfWord = false;
}
Related
How can I sort a list alphabetically based on the Hebrew language in Dart?
Context In Dart, if I have a list: final myList = ['b', 'a']; and I wanted to sort it alphabetically, I would use: myList.sort( (String a, String b) => a.compareTo(b), ); The output of myList is now: ['a', 'b'] Now, this works on letters that are in the English alphabet. Question But if I have a list that's in Hebrew: final unorderedHebAlphabet = ['א', 'ב']; I can't sort it as above using with: unorderedHebAlphabet.sort((String a, String b) => a.compareTo(b)) It doesn't sort. Expected output, instead of: ['א', 'ב'] Should be: ['ב', 'א'] How can I sort a list Alphabetically in the Hebrew language? Notes As a reference, the Hebrew alphabet sorted would be in this order: final sortedHebrewAlphabet = [ 'א', 'ב', 'ג', 'ד', 'ה', 'ו', 'ז', 'ח', 'ט', 'י', 'כ', 'ל', 'מ', 'נ', 'ס', 'ע', 'פ', 'צ', 'ק', 'ר', 'ש', 'ת', ];
It does sort (by UTF-16 code units), but it's being shown in an unintuitive way. final unorderedHebAlphabet = ['א', 'ב']; seems to be parsed RTL, so in the constructed list, element 0 is א and element 1 is ב. That's already the desired order, so sorting it does not change it. (Mixing LTR and RTL text is confusing.) For example: import 'package:collection/collection.dart'; void main() { var literal = ['א', 'ב']; print(literal[0]); // Prints: א print(literal[1]); // Prints: ב const alef = 'א'; const bet = 'ב'; const expectedOrder = [alef, bet]; const listEquals = ListEquality(); print(listEquals.equals(literal..sort(), expectedOrder)); // Prints: true print(listEquals.equals([bet, alef]..sort(), expectedOrder)); // Prints: true } You also can observe that the elements are printed in the correct order if you prefix output with the Unicode LTR override (U+202D) to force rendering the text as LTR. Compare: const ltr = '\u202D'; print('$expectedOrder'); print('$ltr$expectedOrder'); Or you could simply print the elements separately: expectedOrder.forEach(print); which prints: א ב I'm not experienced with dealing with RTL text, but I'd probably avoid mixing LTR and RTL text in code and instead express them as hexadecimal Unicode code points to avoid confusion.
It is being printed out in correct order. It's the Console which is printing it in the way it is. When you save the sorted list into a file and read it again, you get the אב characters as first two characters. So they are being written/iterated in right order. If you print the values one letter per line, you would get desired result. I am not 100% sure how console is handling it, but dart is simply passing it to the STDOUT (default console), and console is detecting that the string is actually RTL and printing (or writing to STDOUT) in that way. I did little fiddling which you can see: import 'dart:io'; void main(List<String> arguments) { final myList = [ 'א', 'ב', 'ג', 'ד', 'ה', 'ו', 'ז', 'ח', 'ט', 'י', 'כ', 'ל', 'מ', 'נ', 'ס', 'ע', 'פ', 'צ', 'ק', 'ר', 'ש', 'ת' ]; // for (var i in myList) { // print("$i => ${i.runes.toList()}"); // } myList.sort(); String s = ""; for (var i in myList) { s += i; } print('$s'); print(myList.first); final file = new File('/Users/rahul/Desktop/string_l/test.txt'); file.writeAsStringSync(s, flush: true); final read = file.readAsStringSync().substring(0, 2); print(read); // You can also verify by calling print with every element for (var i in myList) { print(i); } } output אבגדהוזחטיכלמנסעפצקרשת א אב א ב ג ד ה ו ז ח ט י כ ל מ נ ס ע פ צ ק ר ש ת
import 'dart:io'; final unorderedHebAlphabet = [ 'ב', 'ג', 'ד', 'ה', 'ו', 'ז', 'ח', 'ט', 'י', 'כ', 'ל', 'מ', 'נ', 'ס', 'ע', 'פ', 'צ', 'ק', 'ר', 'ש', 'ת', 'א' ]; void main() { print(unorderedHebAlphabet); unorderedHebAlphabet .sort((a, b) => a.codeUnitAt(0).compareTo(b.codeUnitAt(0))); print(unorderedHebAlphabet); }
how can I check if any word of a string matches a word in a given list?
for example if I have a string = "I Like To Play Football" and a list = [Car,Ball,Door,Sky] it should give true.
Use any of list var list = ["Car","Ball","Door","Sky"]; String text = "i like to play football"; if (list.any((item) => text.toLowerCase().contains(item))) { //Text has a value from list }
Here You Go: void main() { final String tempString = "I Like To Play Football"; final List<String> tempList = ["Car","Ball","Door","Sky"]; for(var i=0; i < tempList.length; i ++) { if(tempString.contains(tempList.elementAt(i).toLowerCase())){ print("Found and its ${tempList[i]}"); } } }
Regex is your friend here. You can make a simple regex that uses each string in the array as an option (and make it case insensitive) then run the match. I've made an example here in javascript, but it's easy to do in dart https://api.dart.dev/stable/2.16.1/dart-core/RegExp-class.html const source = "I Like To Play Football"; const toMatch = ["Car","Ball","Door","Sky"]; let regexString = ''; for (const option of toMatch) { //adding | modifier after string. Last one is redundant of course //also I'm not checking for special regex characters in toMatch, but that might be necessary. regexString += option + '|'; } // using slice to remove last | console.log(regexString.slice(0, -1)); const regexp = new RegExp(regexString.slice(0, -1), 'i'); console.log(source.match(regexp));
Here's a short version: var src = 'I Like To Play Football'.split(' '); var list = ['Car','Ball','Door','Sky']; var result = list.any((x) => src.any((y) => y.toLowerCase().contains(x.toLowerCase()))); print(result);
Dart / Flutter : Waiting for a loop to be completed before continuing... (Async Await?)
I have a function which creates a sublist from a large(very large list). After creating this list, the function goes on treating it (deleting duplicates, sorting...). As long as the list was not too big, it worked fine. But now, I get "The Getter length was called on null". I suppose, it's because the second part of the function (after the loop) starts before the sublist is completed... so it doesn't work... How can we force the function to wait for the loop to be over to continue the rest of the treatment ? Is it with Async /Await ? Or can we do something like "While... something is not over...", or "As soon as something is done... do that" ? (My suggestions might be naive, but I am a beginner...) Here is the code : List themeBankFr() { List<Map> themeBankFr = []; for (Word word in wordBank) { for (Thematique wordTheme in word.theme) { themeBankFr.add({ 'themeFr': wordTheme.themeFr, 'image': wordTheme.image, }); } } // convert each item to a string by using JSON encoding final jsonList = themeBankFr.map((item) => jsonEncode(item)).toList(); // using toSet - toList strategy final uniqueJsonList = jsonList.toSet().toList(); // convert each item back to the original form using JSON decoding final result = uniqueJsonList.map((item) => jsonDecode(item)).toList(); // sort the list of map in alphabetical order result.sort((m1, m2) { var r = m1['themeFr'].compareTo(m2['themeFr']); if (r != 0) return r; return m1['image'].compareTo(m2['image']); }); return result; }
i think i have a good answer that may helps you and it will as following first create another function to do the work of for loops and this function returns a future of list that you need like below Future<List<Map>> futureList(List wordBank){ List<Map> themeBankFr = []; for (Word word in wordBank) { for (Thematique wordTheme in word.theme) { themeBankFr.add({ 'themeFr': wordTheme.themeFr, 'image': wordTheme.image, }); } } return Future.value(themeBankFr); } after that you can use this function inside your code and use it as async await and now you will never run the below lines before you return this array like below List themeBankFr() async { List<Map> themeBankFr = await futureList(wordBank); // convert each item to a string by using JSON encoding final jsonList = themeBankFr.map((item) => jsonEncode(item)).toList(); // using toSet - toList strategy final uniqueJsonList = jsonList.toSet().toList(); // convert each item back to the original form using JSON decoding final result = uniqueJsonList.map((item) => jsonDecode(item)).toList(); // sort the list of map in alphabetical order result.sort((m1, m2) { var r = m1['themeFr'].compareTo(m2['themeFr']); if (r != 0) return r; return m1['image'].compareTo(m2['image']); }); return result; } i think this will solve your problem and i hope this useful for you
Extract number and separate with comma from list in Flutter
List listFinal = []; So listFinal have values from multiple list inside like below. [["test: 111-333-5555", "test2: 222-333-4555"], ["test3: 555-333-2222"]] How do I make this list so that it only extract numbers and separate with comma? End result should be like [1113335555, 2223334555, 5553332222] I can think of trimming or regexp but not sure how to pull this off. many thanks.
Try this void main() { List<String> numberList=[]; List<List<dynamic>> demoList=[["test: 111-333-5555", "test2: 222-333-4555"], ["test3: 555-333-2222"]]; for(int i=0;i<demoList.length;i++){ numberList.addAll(demoList[i].map((e) => e.toString().split(":")[1].replaceAll("-", "")).toList()); } print(numberList.toString()); }
Here is an example to get you started. This doesn't handle things like malformed input strings. First step is to "flatten" the list with .expand, and then for each element of the flattened iterable use a regex to extract the substring. Other options might include using .substring to extract exactly the last 12 characters of the String. You can see this in action on dartpad. void main() { final input = [ ['test: 111-333-5555', 'test2: 222-333-4555'], ['test3: 555-333-2222'] ]; final flattened = input.expand((e) => e); // un-nest the lists // call extractNumber on each element of the flattened iterable, // then collect to a list final result = flattened.map(extractNumber).toList(); print(result); } final _numberRegExp = RegExp(r'.*: ([\d-]+)$'); int extractNumber(String description) { var numberString = _numberRegExp.firstMatch(description).group(1); return int.parse(numberString.replaceAll('-', '')); }
Let's do this in a simple way. List<List<String>> inputList = [ ["test: 111-333-5555", "test2: 222-333-4555"], ["test3: 555-333-2222"] ]; List resultList = []; print('Input List : $inputList'); inputList.forEach((subList){ subList.forEach((element){ var temp = element.split(' ')[1].replaceAll('-', ''); resultList.add(temp); }); }); print('Output List : $resultList'); Here I have taken your list as inputList and stored the result in resultList. For each element of inputList we get a sub-list. I have converted the elements of that sub-list into the needed format and added those into a List. Happy Coding :)
declare variable to store linq entity for conditional statements
I am trying to look up record using if I have the key then use Find if not use Where private ApplicationDbContext db = new ApplicationDbContext(); public bool DeactivatePrice(int priceId = 0, string sponsorUserName = "") { var prices = db.BeveragePrices; // if we have an id then find if (priceId != 0) { prices = prices.Find(priceId); } else { prices = prices.Where(b => b.UserCreated == sponsorUserName); } if (prices != null) { // do something } return true; I get the following error for prices = prices.Find(priceId); Cannot convert app.Model.BeveragePrices from system.data.entity.dbset I am copying the pattern from this answer but something must be different.
Seems you forgot to put a predicate inside the Find function call. Also you need to do ToList on the collection. The second option is a lot more efficient. The first one gets the whole collection before selection. Another note commented by #Alla is that the find returns a single element. So I assume another declaration had been made for 'price' in the first option I state down here. price = prices.ToList.Find(b => b.PriceId == priceId); Or prices = prices.Select(b => b.PriceId == priceId); I assume the field name is PriceId.