diff --git a/.gitignore b/.gitignore index 36f971e..74bb5b7 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ bin/* +main \ No newline at end of file diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json index 8ab5546..e0d4d62 100644 --- a/.vscode/c_cpp_properties.json +++ b/.vscode/c_cpp_properties.json @@ -6,10 +6,10 @@ "${workspaceFolder}/**" ], "defines": [], - "compilerPath": "/usr/bin/clang", + "compilerPath": "/usr/bin/gcc", "cStandard": "c11", "cppStandard": "c++11", - "intelliSenseMode": "clang-x64" + "intelliSenseMode": "gcc-x64" } ], "version": 4 diff --git a/.vscode/settings.json b/.vscode/settings.json index 2bc9e8e..afc8e51 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -59,6 +59,28 @@ "thread": "cpp", "typeinfo": "cpp", "valarray": "cpp", - "variant": "cpp" + "variant": "cpp", + "bitset": "cpp", + "codecvt": "cpp", + "iomanip": "cpp", + "__bit_reference": "cpp", + "__config": "cpp", + "__debug": "cpp", + "__errc": "cpp", + "__functional_base": "cpp", + "__hash_table": "cpp", + "__locale": "cpp", + "__mutex_base": "cpp", + "__node_handle": "cpp", + "__nullptr": "cpp", + "__split_buffer": "cpp", + "__string": "cpp", + "__threading_support": "cpp", + "__tree": "cpp", + "__tuple": "cpp", + "ios": "cpp", + "locale": "cpp", + "queue": "cpp", + "stack": "cpp" } } \ No newline at end of file diff --git a/Makefile b/Makefile index a9dafcd..ccbf411 100644 --- a/Makefile +++ b/Makefile @@ -5,6 +5,7 @@ BIN := bin SRC := src INCLUDE := include LIB := lib +LANG := en_gb LIBRARIES := @@ -14,7 +15,7 @@ else EXECUTABLE := main endif -all: $(EXECUTABLE) +all: $(BIN)/$(EXECUTABLE) clean: $(RM) $(BIN)/$(EXECUTABLE) @@ -22,5 +23,5 @@ clean: run: all ./$(BIN)/$(EXECUTABLE) -$(EXECUTABLE): $(SRC)/*.cpp - $(CC) $(C_FLAGS) -I$(INCLUDE) -L$(LIB) $^ -o $@ $(LIBRARIES) \ No newline at end of file +$(BIN)/$(EXECUTABLE): $(SRC)/*.cpp + $(CC) $(C_FLAGS) -I$(INCLUDE) -L$(LIB) $^ languages/$(LANG)/language_setup.cpp -o $@ $(LIBRARIES) \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..c137129 --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +# Current state +- Reads a string of words referring to a numeral and prints the digit equivalent. + +For example: +`./bin/main "two hundred thousand, four hundred forty"` prints *200440*. + +# To do + +## Mixed text reading +- Expect to read a mixed text (numeral and plain text mixed together), maybe as a file or as a direct input +- Separate said mixed text into a list of chunks of pure plain text, and pure numeral string of words +- Split the list of chunks in 2 recursively +- For each leaf (chunk), evaluate the chunk (plain text evaluates to itself; numeral string evaluates to the equivalent integer value, converted back into a string) +- At every split, concatenate evaluations of both branches +- Parallelize on each split + +## Proper testing +- Move the collection of assert calls & cout calls, away from the main file into proper test files +- End goal: use GTest + +## Cleanup +- Add const keywords to non-modified arguments (like the "text" in the NumeralManager::analyze_numeral, or the "words" in the Trie operations) +- Properly alter types to better match their functions (like adding "unsigned" to many "pos" int arguments) +- Better implement the language-agnostic setup (make NumeralManager to read from a file, instead of having a separate .cpp file only with the "language_setup" function) +- Error handling +- Make the whole CLI interface (help/usage tooltip message, options to read a [list of] string[s] or a [list of] file[s], ...) diff --git a/include/numeral_index.hpp b/include/numeral_index.hpp deleted file mode 100644 index 92d7b9b..0000000 --- a/include/numeral_index.hpp +++ /dev/null @@ -1,16 +0,0 @@ - -#ifndef NUMERAL_INDEX_HPP -#define NUMERAL_INDEX_HPP - -#include -#include - -class NumeralIndex { - public: - bool add(std::string word, int value); - - private: - std::unordered_map index(); -}; - -#endif // !NUMERAL_INDEX_HPP \ No newline at end of file diff --git a/include/numeral_manager.hpp b/include/numeral_manager.hpp new file mode 100644 index 0000000..c1ddc43 --- /dev/null +++ b/include/numeral_manager.hpp @@ -0,0 +1,39 @@ +#ifndef NUMERAL_MANAGER_HPP +#define NUMERAL_MANAGER_HPP + +#include +#include +#include "trie.hpp" +#include "numeral_word.hpp" +#include "numeral_number.hpp" +#include "numeral_modifier.hpp" + +class NumeralManager +{ +public: + bool isNumeralWord(std::string word); + static NumeralManager &getInstance(); + NumeralWord *analyze(std::string &text); + NumeralManager() : _numeral_exclusive(), + _context_dependent(), + _numeral_context(false), + _values(), + _modifiers(){}; + void language_setup(); + +private: + bool add(std::string word, int value, bool exclusive); // numbers + bool add(std::string word, NumeralModifier::Join joiner, bool exclusive); // modifiers + NumeralWord *analyze_numeral(std::string &text, int &pos, bool &alnum_state, bool &is_alnum, int &val); + + // some words may be exclusively numeral ("one"), while others may + // be context dependent ("and"), this is, they may appear outside + // numerals + Trie _numeral_exclusive; + Trie _context_dependent; + bool _numeral_context; + std::unordered_map _values; + std::unordered_map _modifiers; +}; + +#endif // !NUMERAL_MANAGER_HPP \ No newline at end of file diff --git a/include/numeral_modifier.hpp b/include/numeral_modifier.hpp index 41296a4..9cc1d59 100644 --- a/include/numeral_modifier.hpp +++ b/include/numeral_modifier.hpp @@ -1,21 +1,29 @@ - #ifndef NUMERAL_MODIFIER_HPP #define NUMERAL_MODIFIER_HPP #include "numeral_word.hpp" -class NumeralModifier : public virtual NumeralWord { - public: - enum Join {ADD, SUBSTRACT}; +class NumeralModifier : public virtual NumeralWord +{ +public: + enum Join + { + ADD, + SUBSTRACT + }; - NumeralModifier(std::string word, Join joiner); + NumeralModifier(std::string repr, NumeralWord *left, Join joiner) : NumeralWord(repr), _left(left), _right(nullptr), _joiner(joiner){}; + NumeralModifier(std::string repr, Join joiner) : NumeralModifier(repr, nullptr, joiner){}; + ~NumeralModifier(); + void setChild(NumeralWord *child); int evaluate(); + std::string to_string(); - private: - Join _joiner; +private: NumeralWord *_left, *_right; + Join _joiner; - int evaluate(NumeralWord* child); + int evaluate(NumeralWord *child); }; #endif // !NUMERAL_MODIFIER_HPP diff --git a/include/numeral_number.hpp b/include/numeral_number.hpp index 9c2cdc1..ec335ac 100644 --- a/include/numeral_number.hpp +++ b/include/numeral_number.hpp @@ -1,17 +1,21 @@ - #ifndef NUMERAL_NUMBER_HPP #define NUMERAL_NUMBER_HPP #include "numeral_word.hpp" -class NumeralNumber : public virtual NumeralWord { - public: - NumeralNumber(std::string word); +class NumeralNumber : public virtual NumeralWord +{ +public: + NumeralNumber(std::string repr, NumeralWord *child, int value) : NumeralWord(repr), _power(child), _value(value){}; + NumeralNumber(std::string repr, int value) : NumeralNumber(repr, nullptr, value){}; + ~NumeralNumber(); + void setChild(NumeralWord *child); int evaluate(); + std::string to_string(); - private: +private: + NumeralWord *_power; int _value; - NumeralNumber* _power; }; #endif // !NUMERAL_NUMBER_HPP diff --git a/include/numeral_word.hpp b/include/numeral_word.hpp index 235969e..d16acc2 100644 --- a/include/numeral_word.hpp +++ b/include/numeral_word.hpp @@ -1,17 +1,19 @@ - #ifndef NUMERAL_WORD_HPP #define NUMERAL_WORD_HPP #include -class NumeralWord { - public: - NumeralWord(std::string word) : _word(word) {}; - virtual ~NumeralWord(); - virtual int evaluate(); +class NumeralWord +{ +public: + NumeralWord(std::string repr) : _repr(repr){}; + virtual ~NumeralWord(){}; + virtual int evaluate() = 0; + virtual void setChild(NumeralWord *child) = 0; + virtual std::string to_string() = 0; - protected: - std::string _word; +protected: + std::string _repr; }; -#endif // !NUMERAL_WORD_HPP \ No newline at end of file +#endif // !NUMERAL_WORD_HPP diff --git a/include/trie.hpp b/include/trie.hpp index 345cc9b..5ad54e2 100644 --- a/include/trie.hpp +++ b/include/trie.hpp @@ -1,109 +1,23 @@ - #ifndef TRIE_HPP #define TRIE_HPP -#include -#include - -class Trie { - public: - - private: - struct TrieNode - { - bool is_leaf; - std::unordered_map children; - - TrieNode() : is_leaf(false) {}; - - TrieNode* next(char c) { - if (children.find(c) != children.end()) { - return children[c]; - } - - TrieNode* new_child = new TrieNode; - children[c] = new_child; - - return new_child; - } - - bool insert(const std::string &word, int &pos) { - if (pos == word.length()) { - if (!is_leaf) { - is_leaf = true; - return true; - } - return false; - } - else { - char c = word[pos]; - TrieNode* next_node = next(c); - pos++; - - return next_node->insert(word, pos); - } - } - - bool hasChildren() { - // get iterator to first element. If value is non-empty - // => there is at least 1 value => has children. - // faster than looking at map size - for(auto it : children) { - if(it.second != nullptr) { - return true; - } - } - - return false; - } - - bool remove(const std::string &word, int &pos) { - // base case: end of string - if (pos == word.length() && is_leaf) { - is_leaf = false; - return !hasChildren(); //to be deleted if no children - } - - // otherwise keep going - char c = word[pos]; - pos++; - TrieNode* child = children[c]; - - // found nothing - if (child == nullptr) { - return false; - } - - // if child has the following node, gets marked for removal, - // and child itself is not a leaf - if (child->children.find(c) != child->children.end() && - child->remove(word, pos) && - !child->is_leaf) { - delete child; - return true; - } - } - - bool search(const std::string &word, int &pos) { - if(pos == word.length()) { - return is_leaf; - } - - char c = word[pos]; - pos++; - TrieNode* child = children[c]; - - if (child == nullptr) { - return false; - } - - return child->search(word, pos); - } - }; +#include +#include +#include "trie_node.hpp" - TrieNode* root; - +class Trie +{ +public: + Trie() : _root(new TrieNode){}; bool insert(std::string word); + bool remove(std::string word); + bool search(std::string word); + bool empty(); + int size(); + void print(); + +private: + TrieNode *_root; }; #endif // !TRIE_HPP \ No newline at end of file diff --git a/include/trie_node.hpp b/include/trie_node.hpp new file mode 100644 index 0000000..ccd3acb --- /dev/null +++ b/include/trie_node.hpp @@ -0,0 +1,22 @@ +#ifndef TRIE_NODE_HPP +#define TRIE_NODE_HPP + +#include + +class TrieNode +{ +public: + TrieNode() : _is_leaf(false), _children(){}; + TrieNode *next(char c); + bool hasChildren(); + bool insert(const std::string &word, int &pos); + bool remove(const std::string &word, int &pos, bool &to_remove); + bool search(const std::string &word, int &pos); + int size(); + +private: + bool _is_leaf; + std::unordered_map _children; +}; + +#endif // !TRIE_NODE_HPP \ No newline at end of file diff --git a/languages/en_gb/language_setup.cpp b/languages/en_gb/language_setup.cpp new file mode 100644 index 0000000..d4922cf --- /dev/null +++ b/languages/en_gb/language_setup.cpp @@ -0,0 +1,32 @@ +#include "numeral_manager.hpp" +#include + +void NumeralManager::language_setup() +{ + add("one", 1, true); + add("two", 2, true); + add("three", 3, true); + add("four", 4, true); + add("five", 5, true); + add("six", 6, true); + add("seven", 7, true); + add("eight", 8, true); + add("nine", 9, true); + + add("ten", 10, true); + add("twenty", 20, true); + add("thirty", 30, true); + add("fourty", 40, true); + add("forty", 40, true); + add("fifty", 50, true); + add("sixty", 60, true); + add("seventy", 70, true); + add("eighty", 80, true); + add("ninety", 90, true); + + add("hundred", 100, true); + + add("thousand", 1000, true); + + add("and", NumeralModifier::ADD, false); +} \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 1afb8ee..5817922 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,5 +1,74 @@ #include +#include +#include "trie.hpp" +#include "numeral_manager.hpp" -int main(int argc, char *argv[]) { - std::cout << "Hello Easy C++ project!" << std::endl; -} \ No newline at end of file +void trie_test() +{ + Trie t = Trie(); + + assert(t.insert("hello") == true); + assert(t.search("hello") == true); + assert(t.insert("helloworld") == true); + assert(t.search("helloworld") == true); + assert(t.search("helll") == false); + assert(t.insert("hell") == true); + assert(t.insert("h") == true); + assert(t.search("h") == true); + + assert(t.insert("hell") == false); + + assert(t.remove("hello") == true); + assert(t.search("hello") == false); + assert(t.search("helloworld") == true); + assert(t.search("hell") == true); + + assert(t.remove("h") == true); + assert(t.search("h") == false); + assert(t.search("hell") == true); + assert(t.search("helloworld") == true); + + assert(t.remove("hells") == false); + assert(t.remove("tomatoes") == false); + + assert(t.remove("helloworld") == true); + assert(t.search("helloworld") == false); + assert(t.search("hell") == true); + + assert(t.remove("hell") == true); + assert(t.empty() == true); +} + +void num_mgr_test(NumeralManager &mgr) +{ + std::string str = "one hundred and eight"; + NumeralWord *nw = mgr.analyze(str); + //std::cout << nw->to_string() << " = " << nw->evaluate() << std::endl; + assert(nw->evaluate() == 108); + delete nw; + + str = "two hundred thousand, four hundred forty"; + nw = mgr.analyze(str); + assert(nw != nullptr); + //std::cout << nw->to_string() << " = " << nw->evaluate() << std::endl; + assert(nw->evaluate() == 200440); +} + +int main(int argc, char *argv[]) +{ + trie_test(); + + NumeralManager mgr = NumeralManager(); + mgr.language_setup(); + + num_mgr_test(mgr); + + if (argc > 1) + { + std::string str(argv[1]); + int num = mgr.analyze(str)->evaluate(); + std::cout << std::to_string(num) << std::endl; + } + + return 0; +} diff --git a/src/numeral_manager.cpp b/src/numeral_manager.cpp new file mode 100644 index 0000000..242d865 --- /dev/null +++ b/src/numeral_manager.cpp @@ -0,0 +1,131 @@ +#include +#include "numeral_manager.hpp" + +bool NumeralManager::add(std::string word, int value, bool exclusive) +{ + bool success = false; + if (exclusive) + { + success = _numeral_exclusive.insert(word); + } + else + { + success = _context_dependent.insert(word); + } + + _values[word] = value; + return success; +} + +bool NumeralManager::add(std::string word, NumeralModifier::Join joiner, bool exclusive) +{ + bool success = false; + if (exclusive) + { + success = _numeral_exclusive.insert(word); + } + else + { + success = _context_dependent.insert(word); + } + + _modifiers[word] = joiner; + return success; +} + +bool NumeralManager::isNumeralWord(std::string word) +{ + bool exclusive_find = _numeral_exclusive.search(word); + + return exclusive_find || (_numeral_context && _context_dependent.search(word)); +} + +NumeralWord *NumeralManager::analyze_numeral(std::string &text, int &pos, bool &alnum_state, bool &is_alnum, int &last_val) +{ + std::string word = ""; + NumeralWord *numeral = nullptr; + int val = last_val; + int old_pos = pos; + alnum_state = std::isalnum(text[pos]); + is_alnum = std::isalnum(text[pos]); + + while (pos < text.length()) + { + word = ""; + + while (pos < text.length() && is_alnum == alnum_state) + { + word += text[pos]; + pos++; + is_alnum = std::isalnum(text[pos]); + } + + if (alnum_state == true && !word.empty()) + { + // If we have read an alphanumeric chunk of characters + if (_modifiers.find(word) != _modifiers.end()) + { + if (numeral == nullptr) + { + numeral = new NumeralModifier(word, _modifiers[word]); + } + else + { + NumeralModifier *mod = new NumeralModifier(word, numeral, _modifiers[word]); + NumeralWord *next_numeral = analyze_numeral(text, pos, alnum_state, is_alnum, val); + mod->setChild(next_numeral); + + numeral = mod; + } + } + else if (_values.find(word) != _values.end()) + { + val = _values[word]; + + if (numeral == nullptr) + { + numeral = new NumeralNumber(word, val); + } + else if (val >= last_val) + { + // Assumes eurocentric property where bigger values + // following smaller values is a multiplication + // (like in "two hundred") + NumeralNumber *num = new NumeralNumber(word, val); + numeral->setChild(num); + } + else + { + // Assumes eurocentric property where smaller values + // following bigger values is a sum (like in "forty two") + NumeralModifier *mod = new NumeralModifier("+", numeral, NumeralModifier::ADD); + + NumeralWord *next_numeral = analyze_numeral(text, old_pos, alnum_state, is_alnum, val); + pos = old_pos; + mod->setChild(next_numeral); + + numeral = mod; + } + + last_val = val; + } + } + + old_pos = pos; + + alnum_state = std::isalnum(text[pos]); + } + + return numeral; +} + +NumeralWord *NumeralManager::analyze(std::string &text) +{ // We assume that this is called on a text that we already know is entirely a numeral + std::string word = ""; + int pos = 0; + bool alnum_state = std::isalnum(text[pos]); + bool is_alnum = std::isalnum(text[pos]); + int last_val = 0; + + return analyze_numeral(text, pos, alnum_state, is_alnum, last_val); +} \ No newline at end of file diff --git a/src/numeral_modifier.cpp b/src/numeral_modifier.cpp index 7cce61c..7a8552a 100644 --- a/src/numeral_modifier.cpp +++ b/src/numeral_modifier.cpp @@ -1,27 +1,42 @@ - #include "numeral_modifier.hpp" -NumeralModifier::NumeralModifier(std::string word, Join joiner) : NumeralWord(word), _joiner(joiner) {}; - -NumeralModifier::~NumeralModifier() { - if (_left != nullptr) { +NumeralModifier::~NumeralModifier() +{ + if (_left != nullptr) + { delete _left; } - if (_right != nullptr) { + if (_right != nullptr) + { delete _right; } } -int NumeralModifier::evaluate(NumeralWord* child) { - if (child == nullptr) { +void NumeralModifier::setChild(NumeralWord *child) +{ + if (_right != nullptr) + { + _right->setChild(child); + } + else + { + _right = child; + } +} + +int NumeralModifier::evaluate(NumeralWord *child) +{ + if (child == nullptr) + { return 0; } - + return child->evaluate(); } -int NumeralModifier::evaluate() { +int NumeralModifier::evaluate() +{ switch (_joiner) { case ADD: @@ -31,10 +46,33 @@ int NumeralModifier::evaluate() { case SUBSTRACT: return evaluate(_left) - evaluate(_right); //break; - + default: return 0; //break; } } +std::string NumeralModifier::to_string() +{ + std::string out = _repr + "("; + if (_left != nullptr) + { + out += _left->to_string(); + } + else + { + out += "null"; + } + out += std::string(","); + if (_right != nullptr) + { + out += _right->to_string(); + } + else + { + out += "null"; + } + out += std::string(")"); + return out; +} diff --git a/src/numeral_number.cpp b/src/numeral_number.cpp index 252bdba..ae27c11 100644 --- a/src/numeral_number.cpp +++ b/src/numeral_number.cpp @@ -1,18 +1,46 @@ #include "numeral_number.hpp" -NumeralNumber::NumeralNumber(std::string word) : NumeralWord(word) {}; - -NumeralNumber::~NumeralNumber() { - if (_power != nullptr) { +NumeralNumber::~NumeralNumber() +{ + if (_power != nullptr) + { delete _power; } } -int NumeralNumber::evaluate() { - if (_power != nullptr) { +void NumeralNumber::setChild(NumeralWord *child) +{ + if (_power != nullptr) + { + _power->setChild(child); + } + else + { + _power = child; + } +} + +int NumeralNumber::evaluate() +{ + if (_power != nullptr) + { return _value * _power->evaluate(); } return _value; +} + +std::string NumeralNumber::to_string() +{ + std::string out = _repr + "*"; + if (_power != nullptr) + { + out += _power->to_string(); + } + else + { + out += "null"; + } + return out; } \ No newline at end of file diff --git a/src/trie.cpp b/src/trie.cpp index 827cb91..d171f4e 100644 --- a/src/trie.cpp +++ b/src/trie.cpp @@ -1,2 +1,30 @@ - #include "trie.hpp" + +bool Trie::insert(std::string word) +{ + int pos = 0; + return _root->insert(word, pos); +} + +bool Trie::remove(std::string word) +{ + int pos = 0; + bool to_remove = false; + return _root->remove(word, pos, to_remove); +} + +bool Trie::search(std::string word) +{ + int pos = 0; + return _root->search(word, pos); +} + +bool Trie::empty() +{ + return !_root->hasChildren(); +} + +int Trie::size() +{ + return _root->size(); +} diff --git a/src/trie_node.cpp b/src/trie_node.cpp new file mode 100644 index 0000000..67ce93f --- /dev/null +++ b/src/trie_node.cpp @@ -0,0 +1,101 @@ +#include "trie_node.hpp" + +typedef std::unordered_map::iterator ChildIt; + +TrieNode *TrieNode::next(char c) +{ + if (_children.find(c) != _children.end()) + { + return _children[c]; + } + + TrieNode *new_child = new TrieNode; + _children[c] = new_child; + + return new_child; +} + +bool TrieNode::hasChildren() +{ + return _children.size() > 0; +} + +bool TrieNode::insert(const std::string &word, int &pos) +{ + if (pos == word.length()) + { + if (!_is_leaf) + { + _is_leaf = true; + return true; + } + return false; + } + else + { + char c = word[pos]; + TrieNode *next_node = next(c); + pos++; + + return next_node->insert(word, pos); + } +} + +bool TrieNode::remove(const std::string &word, int &pos, bool &to_remove) +{ + // base case: end of string + if (pos == word.length() && _is_leaf) + { + _is_leaf = false; + to_remove = !hasChildren(); //to be deleted if no _children + return true; + } + + // otherwise keep going + char c = word[pos]; + pos++; + ChildIt it = _children.find(c); + + // found nothing + if (it == _children.end()) + { + to_remove = false; + return false; + } + + bool success = it->second->remove(word, pos, to_remove); + + // if child gets marked for removal + if (to_remove) + { + delete it->second; // delete the pointer + _children.erase(it); // remove the entry + to_remove = !_is_leaf && !hasChildren(); // remove if not a leaf either and no more _children remaining + } + + return success; +} + +bool TrieNode::search(const std::string &word, int &pos) +{ + if (pos == word.length()) + { + return _is_leaf; + } + + char c = word[pos]; + pos++; + ChildIt it = _children.find(c); + + if (it == _children.end()) + { + return 0; + } + + return it->second->search(word, pos); +} + +int TrieNode::size() +{ + return _children.size(); +}