7
\$\begingroup\$

The code below shows my take at creating a wordsearch puzzle solver, where the wordearch is embedded into a .txt file that is parsed and fit into a vector<vector<char>>. Then, the inputted word is searched for.

What would be the best way, in terms of effective OOP, to separate the program? Should I create two separate .cpp files? - one to store the search functions and another to store the iterate function? Moreover, what is the best way to present the user interface - the main() function? Should that also be separated into another file?

Also, what could it use syntactically? I think the redefinition of location in the iterate function might be a bit redundant and can be reduced.

/*

 Wordsearch Solver

 Program aims to find any inputted word within a .txt file containing square
 or rectangular puzzles, with characters not separated by spaces. If the word
 is found, prints the position of the first character of the word. Otherwise,
 prints -1 and -1. The eight common directions are tested - left, right, up,
 down, diagonally left-up, diagonally left-down, diagonally right-down, and
 diagonally right-up.

 For the diagonal search, assume a slope of 1. That is,
 1 unit left or right = 1 unit diagonally. To work with rectangular (not square) arrays,
 the minimum of the two diagonal directions is taken to be the number of remaining
 characters.

 Sample 40x40 found at end

 AA

 */

#include <iostream>
using std::cout; using std::endl;

#include <string>
using std::string;

#include <fstream>
using std::ifstream;

#include <sstream>
using std::istringstream;

#include <vector>
using std::vector;

#include <utility>
using std::pair; using std::make_pair;

#include <algorithm>
using std::min;

string spaced(string str) {
    /*
     Inputs a space between a non-space separated string.
     str: input string
     Return: a string with spaces in between the characters.

     Comment out if using .txt file characters already space separated.

     Found on cplusplus.com
     */
    if ( str == "" )
        return str;
    string result( 2 * str.size() - 1, ' ' );

    for ( int i = 0, j = 0; i < str.size(); i++, j+=2 )
        result[j] = str[i];

    return result;
}

vector<vector<char>> open_parse_file(string filename) {
    /*
     Parses a .txt file and creates the associated wordsearch puzzle of type vector<vector<char>>.
     filename: file to parse
     Return: a vector<vector<char>> wordsearch puzzle.
     */
    ifstream input_file;
    string line;

    vector<vector<char>> wordsearch;

    input_file.open(filename);

    while (getline(input_file, line)) {
        vector<char> temp_vec;
        string space_sep_line = spaced(line);
        istringstream iss(space_sep_line);
        char letter;

        while (iss) {
            iss >> letter;
            temp_vec.push_back(letter);
        }

        temp_vec.pop_back();  // iss puts extra char at end; remove it
        wordsearch.push_back(temp_vec);
    }
    return wordsearch;
}

pair<int, int> horizontal_forward_search(vector<vector<char>>  const&wordsearch, int row_num, int col_num, string word) {
    /*
     Searches horizontally and forward for a word matching the inputted word.
     wordsearch: the wordsearch puzzle
     row_num: the current row position in the array
     col_num: the current column position in the array
     word: the input word
     Return: the location
     */
    long remaining_chars = wordsearch[row_num].size() - col_num;

    if (word.size() > remaining_chars + 1)   // word size exceeds remaining characters plus 1 from current postion
        return make_pair(-1, -1);


    else {
        string temp_str = "";
        int cur_pos = col_num;
        int i = 0;
        while (i < word.size()) {
            char cur_letter = wordsearch[row_num - 1][cur_pos - 1];
            temp_str.push_back(cur_letter);
            ++cur_pos;  // next pos in array
            ++i;
        }
        if (temp_str == word)
            return make_pair(row_num, col_num);
    }
    return make_pair(-1,-1);

}

pair<int, int> horizontal_backward_search(vector<vector<char>> const& wordsearch, int row_num, int col_num, string word) {
    /*
     Searches horizontally and backwards for a word matching the inputted word.
     wordsearch: the wordsearch puzzle
     row_num: the current row position in the array
     col_num: the current column position in the array
     word: the input word
     Return: the location
     */
    long remaining_chars = col_num - 1;

    if (word.size() > remaining_chars + 1)
        return make_pair(-1, -1);

    else {
        string temp_str = "";
        int cur_pos = col_num;
        int i = 0;
        while (i < word.size()) {
            char cur_letter = wordsearch[row_num - 1][cur_pos - 1];
            temp_str.push_back(cur_letter);
            --cur_pos;
            ++i;
        }
        if (temp_str == word)
            return make_pair(row_num, col_num);
    }
    return make_pair(-1, -1);
}

pair<int, int> vertical_down_search(vector<vector<char>> const&wordsearch, int row_num, int col_num, string word) {
    /*
     Searches vertically and down for words matching the inputted word.
     wordsearch: the wordsearch puzzle
     row_num: the current row position in the array
     col_num: the current column position in the array
     word: the input word
     Return: the location
     */
    long remaining_chars = wordsearch.size() - row_num;

    if (word.size() > remaining_chars + 1)
        return make_pair(-1, -1);

    else {
        string temp_str = "";
        int cur_pos = row_num;
        int i = 0;
        while (i < word.size()) {
            char cur_letter = wordsearch[cur_pos - 1][col_num - 1];
            temp_str.push_back(cur_letter);
            ++cur_pos;
            ++i;
        }
        if (temp_str == word)
            return make_pair(row_num, col_num);
    }
    return make_pair(-1, -1);
}

pair<int, int> vertical_up_search(vector<vector<char>> const&wordsearch, int row_num, int col_num, string word) {
    /*
     Searches verticlally and up for words matching the inputted word.
     wordsearch: the wordsearch puzzle
     row_num: the current row position in the array
     col_num: the current column position in the array
     word: the input word
     Return: the location
     */
    long remaining_chars = row_num - 1;

    if (word.size() > remaining_chars + 1)
        return make_pair(-1, -1);

    else {
        string temp_str = "";
        int cur_pos = row_num;
        int i = 0;
        while (i < word.size()) {
            char cur_letter = wordsearch[cur_pos - 1][col_num - 1];
            temp_str.push_back(cur_letter);
            --cur_pos;
            ++i;
        }
        if (temp_str == word)
            return make_pair(row_num, col_num);
    }
    return make_pair(-1, -1);
}

pair<int, int> diagonal_left_up_search(vector<vector<char>> const&wordsearch, int row_num, int col_num, string word) {
    /*
     Searches diagonally left and up for words matching the inputted word.
     wordsearch: the wordsearch puzzle
     row_num: the current row position in the array
     col_num: the current column position in the array
     word: the input word
     Return: the location

     Assume slope of 1; 1 unit up or down = 1 unit diagonally.
     */
    long remaining_left = col_num - 1;
    long remaining_up = row_num - 1;
    long remaining_chars = min(remaining_left, remaining_up);

    if (word.size() > remaining_chars + 1)
        return make_pair(-1, -1);

    else {
        string temp_str = "";
        int cur_pos_v = row_num;
        int cur_pos_h = col_num;
        int i = 0;
        while (i < word.size()) {
            char cur_letter = wordsearch[cur_pos_v - 1][cur_pos_h - 1];
            temp_str.push_back(cur_letter);
            --cur_pos_v; //vertically
            --cur_pos_h; //horizontally
            ++i;
        }
        if (temp_str == word)
            return make_pair(row_num, col_num);
    }
    return make_pair(-1, -1);
}

pair<int, int> diagonal_left_down_search (vector<vector<char>> const&wordsearch, int row_num, int col_num, string word) {
    /*
     Searches diagonally left and down for words matching the inputted word.
     wordsearch: the wordsearch puzzle
     row_num: the current row position in the array
     col_num: the current column position in the array
     word: the input word
     Return: the location

     Assume slope of 1; 1 unit up or down = 1 unit diagonally.
     */
    long remaining_left = col_num - 1;
    long remaining_down = wordsearch.size() - row_num;
    long remaining_chars = min(remaining_left, remaining_down);

    if (word.size() > remaining_chars + 1)
        return make_pair(-1, -1);

    else {
        string temp_str = "";
        int cur_pos_v = row_num;
        int cur_pos_h = col_num;
        int i = 0;
        while (i < word.size()) {
            char cur_letter = wordsearch[cur_pos_v - 1][cur_pos_h - 1];
            temp_str.push_back(cur_letter);
            ++cur_pos_v;
            --cur_pos_h;
            ++i;
        }
        if (temp_str == word)
            return make_pair(row_num, col_num);
    }
    return make_pair(-1, -1);
}

pair<int, int> diagonal_right_up_search (vector<vector<char>> const&wordsearch, int row_num, int col_num, string word) {
    /*
     Searches diagonally right and up for words matching the inputted word.
     wordsearch: the wordsearch puzzle
     row_num: the current row position in the array
     col_num: the current column position in the array
     word: the input word
     Return: the location

     Assume slope of 1; 1 unit up or down = 1 unit diagonally.
     */
    long remaining_right = wordsearch[row_num].size() - col_num;
    long remaining_up = row_num - 1;
    long remaining_chars = min(remaining_right, remaining_up);

    if (word.size() > remaining_chars + 1)
        return make_pair(-1, -1);

    else {
        string temp_str = "";
        int cur_pos_v = row_num;
        int cur_pos_h = col_num;
        int i = 0;
        while (i < word.size()) {
            char cur_letter = wordsearch[cur_pos_v - 1][cur_pos_h - 1];
            temp_str.push_back(cur_letter);
            --cur_pos_v;
            ++cur_pos_h;
            ++i;
        }
        if (temp_str == word)
            return make_pair(row_num, col_num);
    }
    return make_pair(-1, -1);
}

pair<int, int> diagonal_right_down_search (vector<vector<char>> const&wordsearch, int row_num, int col_num, string word) {
    /*
     Searches diagonally right and down for words matching the inputted word.
     wordsearch: the wordsearch puzzle
     row_num: the current row position in the array
     col_num: the current column position in the array
     word: the input word
     Return: the location

     Assume slope of 1; 1 unit up or down = 1 unit diagonally.
     */
    long remaining_right = wordsearch[row_num].size() - col_num;
    long remaining_down = wordsearch.size() - row_num;
    long remaining_chars = min(remaining_right, remaining_down);

    if (word.size() > remaining_chars + 1)
        return make_pair(-1, -1);

    else {
        string temp_str = "";
        int cur_pos_v = row_num;
        int cur_pos_h = col_num;
        int i = 0;
        while (i < word.size()) {
            char cur_letter = wordsearch[cur_pos_v - 1][cur_pos_h - 1];
            temp_str.push_back(cur_letter);
            ++cur_pos_v;
            ++cur_pos_h;
            ++i;
        }
        if (temp_str == word)
            return make_pair(row_num, col_num);
    }
    return make_pair(-1, -1);
}

pair<int, int> iterate(vector<vector<char>> const&wordsearch, string word) {
    /*
     Iterates through the wordsearch to find the inputted word
     wordsearch: the wordsearch puzzle
     word: the inputted word
     Return: the location
     */

    int row_num = 0;
    for (vector<char> row: wordsearch) {
        ++row_num;
        int col_num = 0;
        for (char letter: row) {
            ++col_num;
            if (letter == word[0]) {  // first letter of word found, confirm/deny word
                // Perform the 8 possible searches.
                pair<int, int> location = horizontal_forward_search(wordsearch, row_num, col_num, word);
                if (location.first != -1)
                    return location;
                location = horizontal_backward_search(wordsearch, row_num, col_num, word);
                if (location.first != -1)
                    return location;
                location = vertical_down_search(wordsearch, row_num, col_num, word);
                if (location.first != -1)
                    return location;
                location = vertical_up_search(wordsearch, row_num, col_num, word);
                if (location.first != -1)
                    return location;
                location = diagonal_left_up_search(wordsearch, row_num, col_num, word);
                if (location.first != -1)
                    return location;
                location = diagonal_left_down_search(wordsearch, row_num, col_num, word);
                if (location.first != -1)
                    return location;
                location = diagonal_right_up_search(wordsearch, row_num, col_num, word);
                if (location.first != -1)
                    return location;
                location = diagonal_right_down_search(wordsearch, row_num, col_num, word);
                if (location.first != -1)
                    return location;
            }
        }
    }
    return make_pair(-1, -1);
}

int main() {
    // Input information here
    vector<vector<char>> wordsearch = open_parse_file("input text file here");
    pair<int, int> location = iterate(wordsearch, "input word here");
    cout << location.first << " and " << location.second << endl;

} 


/*
 Sample 40x40 wordsearch

 khtjbobelbllelawrlafpsikeujttiefgpedzdud
 urrrzlmqesrtijulktjralhhoxbcupgchakqylin
 mjagawzurirorotsqbogbsacqleejcyshtddkpbg
 qtnknpniunpexeoxxdrzoncntunpqsdsjhmyhfcc
 snitnirptblbmmheuoiyhyvbnintcmxsdzpaijau
 kperformancecoatxzpggmudhilkqhnqovcltrkf
 msupplierkvrljtloxnonsgrtingsfbdyjzcwmly
 dnbcvienehbdriisjmlviineotggpiryhiqmsdlp
 yejzssctpnsdozoduofnpglwzccjiouxrqtpdxhj
 gzgxnsckmaintenanceppgwortegisqfjttetuwb
 npbzdixiesrgomahxssniserolpjxsezzaxigldl
 iqnmwfwotmktnhctsruahcckjtynndmdovkxbcmc
 rkuqdzerjsassemblyobsaqpuqxuigrvmzannsmh
 uawfrcigetartshitmcuqcazsnfespmbkfhyeqxt
 tapvwbgjgdmlgwtiydvyikzvuywugdytzxbntudn
 ccpeufynjgrjptlaqetyhizeacffaqomnxfemcvh
 ambtzwbtbmyalarjbawkvabosyzrpxdnllaqyeel
 fdijsposyiieueupauwkvffbjtmscktfqbvnuryr
 uogubvhiasrqgqxzedpgtjfbsveeibhjkjeyqdsa
 nlsfwxikkffwzwdpqubyfcexdmxebspdbienytxr
 avmuvxraxqwogzzewzpqurndrbmbtoewyknigrgy
 mlfmsvczbnbfatlcqoilvqjgenhxeyxotnndymgd
 tdoylwblacsswaazrsqptrhskjskpacmbyoghxdq
 doqwnpemhtqqmfmstgyveegbzhiszqjrzgfjhgnr
 yzkctjdotnjwzslxjlxvywjpjkovnrsqoqcazwxi
 hldcmmhadrndojumhjvtrsjkzzxwpokahglbcfll
 fghegxihojeqcdeaejzfzeksqgkhetngnmlmkhqb
 erubfrtmbalpnnljfseheugkxqaolhjwzqunleuh
 lfmnqjfbzyypmehzizhyurdsxmioejyeihfotmkc
 ufvtnqgaquifugqpwnpsvbwkplwehrqqpvahrzuh
 ukiwxtnqtztnuaeqdanzpspphaqsiaomsallnalt
 mfxmnbpxwhjlcjxstcxpymgazvlbubdymgdjytga
 nodglgayixlwfndioqswwarbaybtkhqmagoksgos
 auuoyzvwbaxlaalsedsciwdfaqgzypizluekliet
 rdldaqlveysukdutwutkyyjswzdvnfextdojspui
 ipmgltqauypamxssoppeygwgpftounkaxerwfkrt
 cfhiifkyvtvxnegqlmpbbckbexscntziitkrglmh
 zpqlaolbocqijyvbmxegdjnextlmciytfgedfdjg
 ritzgiuksqcaiqvwajjajydolwcxzxlwmdkvchua
 dinflhovqslcmnfigvjsvitfotytytqtccmtrvdl

 */
\$\endgroup\$
  • 1
    \$\begingroup\$ OOB Did you mean OOP? \$\endgroup\$ – dustytrash Aug 14 at 13:20
  • \$\begingroup\$ This crashes too much for me to consider it "working". The row_num reaches 41 for the 40 line sample. If you fix that issue, searching for a long word (e.g. the default "input word here", or just "aaaaaaaaaaa") will crash because it tries to search outside the valid range. \$\endgroup\$ – user673679 Aug 14 at 19:28
  • \$\begingroup\$ @user673679 Can you elaborate? I just tested line 40 and "input word here", and it worked fine. \$\endgroup\$ – Art Aug 14 at 20:21
  • 1
    \$\begingroup\$ I am not sure what he is talking about either. Your code worked right out of the box. Perhaps they made a typo? \$\endgroup\$ – Thomas Kolarik Aug 15 at 2:41
  • 1
    \$\begingroup\$ People, if you find bugs, explain them in answer or comment. But that's not a reason to VTC if multiple people are convinced it roughly works. \$\endgroup\$ – Mast Aug 15 at 11:15
5
\$\begingroup\$

I actually just recently did something like this for a coding challenge.

I chose to create two separate classes. One is a reader class that reads from files and feeds data to a Grid class. The Grid class then initializes itself with all of the possible combinations of words at the start. This then makes it nice and easy for finding words as it just needs to make a call to the "find" on a set.

If you wish to add an additional interface for this then I would add this as a separate class. This class would handle transforming user input into calls to the reader/writer class, which in turn makes calls to the Grid class. If your interface is simple enough (i.e. you only want to be able to input which text file should be read by the your reader/writer class) this can be just done in main() but a separate class is better if you want to make it portable in the future.

Now onto your code specifically, the biggest issue that I see with your implementation is that you have 8 separate functions that do more or less the exact same thing. You should try to abstract this out and have it all done within a single single method. I have an example within my Grid's "GenerateWords" method.

    while (iss) {
        iss >> letter;
        temp_vec.push_back(letter);
    }

If you simply make your check,

    while (iss >> letter) {
        temp_vec.push_back(letter);
    }

you will find that there is not "an extra" letter that is being added to the end.

\$\endgroup\$
0
\$\begingroup\$

There's no need to use 1-based row and column numbers and have to subtract 1 all the time for indexing. Use native (0-based) indexing, and add 1 at the end for display to the user. That makes the code much easier to follow.

Don't pass std::string objects by value. If the function doesn't need a copy, pass by reference to const, just as you have correctly done for the grid.

The only difference between the eight different search directions are the amount by which we advance the row and column to step between letters (these also fully determine the initial test of whether there's room for the word). These eight functions can be reduced to a single function with two additional parameters for Δx and Δy.

The search functions themselves do much more work than they need to. Think about how string comparison normally works: we generally return as soon as we reach a mismatch, rather than continuing to check the rest of the string. Think about how we can fail early, rather than accumulating characters into temp_str (that's a terrible name, by the way).

Have you noticed that the search functions only actually need to return true or false? There's no extra information in passing back the position information that was passed in.

I think your data representation might be holding you back. It's certainly worth creating a Grid class to encapsulate the grid's width, height and contents. If we represent the contents in linear form, we can always access the element at (x,y) using the linear index x * width + y. This is a standard representation used in image processing. If we use that, the Δx and Δy parameters become a single stride parameter (equal to Δx * width + Δy).

Turning now to reading the file, we don't need the spaced function. Instead, read each line as a string, and then make a std::vector<char> from the string.

On a more subjective note, I'm not a fan of those file-level using declarations - keep the scope as small as reasonably possible.

Modified code

Here, I've kept the vector-of-vectors representation, but applied most of the other changes I suggest (and also adapted to read from a fixed input file, for self-contained testing):

#include <istream>
#include <string>
#include <utility>
#include <vector>

using Grid = std::vector<std::vector<char>>;

static Grid read_grid(std::istream& in)
{
    Grid wordsearch;

    std::string line;
    while (in >> line) {
        wordsearch.emplace_back(line.begin(), line.end());
    }

    return wordsearch;
}


static bool search_line(Grid const& grid,
                 int row, int col, const std::string& word,
                 int delta_x, int delta_y)
{
    int end_row = row + delta_y * word.size();
    if (end_row < 0 || static_cast<std::size_t>(end_row) >= grid.size()) return false;
    int end_col = col + delta_x * word.size();
    if (end_col < 0 || static_cast<std::size_t>(end_col) >= grid[end_row].size()) return false;

    for (char c: word) {
        if (grid[row][col] != c)
            return false;
        row += delta_y;
        col += delta_x;
    }
    return true;
}

std::pair<int, int> iterate(Grid const&wordsearch, const std::string& word) {
    /*
     Iterates through the wordsearch to find the inputted word
     wordsearch: the wordsearch puzzle
     word: the inputted word
     Return: the location
     */
    static const std::initializer_list<std::pair<int,int>> directions =
        {{-1, -1}, {-1, 0}, {-1, 1}, {0, -1}, {0, 1}, {1, -1}, {1, 0}, {1, 1}};

    int row_num = 0;
    for (auto const& row: wordsearch) {
        int col_num = 0;
        for (char letter: row) {
            if (letter == word[0]) {  // first letter of word found, confirm/deny word
                // Perform the 8 possible searches.
                for (auto [dx, dy]: directions) {
                    if (search_line(wordsearch, row_num, col_num, word, dx, dy))
                        return std::make_pair(row_num+1, col_num+1);
                }
            }
            ++col_num;
        }
        ++row_num;
    }
    return std::make_pair(-1, -1);
}

#include <iostream>
#include <sstream>
static constexpr const char *const sample_grid =
    "khtjbobelbllelawrlafpsikeujttiefgpedzdud\n"
    "urrrzlmqesrtijulktjralhhoxbcupgchakqylin\n"
    "mjagawzurirorotsqbogbsacqleejcyshtddkpbg\n"
    "qtnknpniunpexeoxxdrzoncntunpqsdsjhmyhfcc\n"
    "snitnirptblbmmheuoiyhyvbnintcmxsdzpaijau\n"
    "kperformancecoatxzpggmudhilkqhnqovcltrkf\n"
    "msupplierkvrljtloxnonsgrtingsfbdyjzcwmly\n"
    "dnbcvienehbdriisjmlviineotggpiryhiqmsdlp\n"
    "yejzssctpnsdozoduofnpglwzccjiouxrqtpdxhj\n"
    "gzgxnsckmaintenanceppgwortegisqfjttetuwb\n"
    "npbzdixiesrgomahxssniserolpjxsezzaxigldl\n"
    "iqnmwfwotmktnhctsruahcckjtynndmdovkxbcmc\n"
    "rkuqdzerjsassemblyobsaqpuqxuigrvmzannsmh\n"
    "uawfrcigetartshitmcuqcazsnfespmbkfhyeqxt\n"
    "tapvwbgjgdmlgwtiydvyikzvuywugdytzxbntudn\n"
    "ccpeufynjgrjptlaqetyhizeacffaqomnxfemcvh\n"
    "ambtzwbtbmyalarjbawkvabosyzrpxdnllaqyeel\n"
    "fdijsposyiieueupauwkvffbjtmscktfqbvnuryr\n"
    "uogubvhiasrqgqxzedpgtjfbsveeibhjkjeyqdsa\n"
    "nlsfwxikkffwzwdpqubyfcexdmxebspdbienytxr\n"
    "avmuvxraxqwogzzewzpqurndrbmbtoewyknigrgy\n"
    "mlfmsvczbnbfatlcqoilvqjgenhxeyxotnndymgd\n"
    "tdoylwblacsswaazrsqptrhskjskpacmbyoghxdq\n"
    "doqwnpemhtqqmfmstgyveegbzhiszqjrzgfjhgnr\n"
    "yzkctjdotnjwzslxjlxvywjpjkovnrsqoqcazwxi\n"
    "hldcmmhadrndojumhjvtrsjkzzxwpokahglbcfll\n"
    "fghegxihojeqcdeaejzfzeksqgkhetngnmlmkhqb\n"
    "erubfrtmbalpnnljfseheugkxqaolhjwzqunleuh\n"
    "lfmnqjfbzyypmehzizhyurdsxmioejyeihfotmkc\n"
    "ufvtnqgaquifugqpwnpsvbwkplwehrqqpvahrzuh\n"
    "ukiwxtnqtztnuaeqdanzpspphaqsiaomsallnalt\n"
    "mfxmnbpxwhjlcjxstcxpymgazvlbubdymgdjytga\n"
    "nodglgayixlwfndioqswwarbaybtkhqmagoksgos\n"
    "auuoyzvwbaxlaalsedsciwdfaqgzypizluekliet\n"
    "rdldaqlveysukdutwutkyyjswzdvnfextdojspui\n"
    "ipmgltqauypamxssoppeygwgpftounkaxerwfkrt\n"
    "cfhiifkyvtvxnegqlmpbbckbexscntziitkrglmh\n"
    "zpqlaolbocqijyvbmxegdjnextlmciytfgedfdjg\n"
    "ritzgiuksqcaiqvwajjajydolwcxzxlwmdkvchua\n"
    "dinflhovqslcmnfigvjsvitfotytytqtccmtrvdl\n";

int main() {
    // Input information here
    std::istringstream infile{sample_grid};
    const Grid wordsearch = read_grid(infile);
    std::pair<int, int> location = iterate(wordsearch, "supplier");
    std::cout << location.first << " and " << location.second << std::endl;

}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.