3

I am trying to generate ASCII art given a string.

art.cpp

#pragma once

#include <string>
#include <vector>

#include "art.h"

std::string Art::display(std::string& text) {
    std::string final_text;
    final_text.reserve(text.length());
    for (char letter: text) {
        std::string single = letters[letter];
        /* tried this */ single.erase(std::remove(single.begin(), single.end(), '\n'), single.end());
        final_text += single;
    }
    return final_text;
}

art.h

#pragma once

#include <string>
#include <vector>
#include <map>

class Art {
public:
    std::map<char, std::string> letters = { 
        std::make_pair('a', std::string(R"(  
    _
   / \
  / _ \
 / ___ \
/__/ \__\
)")),
        std::make_pair('b', std::string(R"(
  ____
 | __ ) 
 |  _ \ 
 | |_) |
 |____/ 
)")),
        std::make_pair('c', std::string(R"(
   ____ 
  / ___|
 | |    
 | |___ 
  \____|
)")),
        std::make_pair('d', std::string(R"(
  ____  
 |  _ \ 
 | | | |
 | |_| |
 |____/  
)")),
        std::make_pair('e', std::string(R"(
  _____ 
 | ____|
 |  _|  
 | |___ 
 |_____|
)")),
        std::make_pair('f', std::string("asdf")),
        std::make_pair('g', std::string("asdf")),
        std::make_pair('h', std::string(R"(
  _   _ 
 | | | |
 | |_| |
 |  _  |
 |_| |_|
)")),
        std::make_pair('i', std::string("asdf")),
        std::make_pair('j', std::string("asdf")),
        std::make_pair('k', std::string(R"(
  _  __
 | |/ /
 | ' / 
 | . \ 
 |_|\_\
)")),
        std::make_pair('l', std::string("asdf")),
        std::make_pair('m', std::string("asdf")),
        std::make_pair('n', std::string("asdf")),
        std::make_pair('o', std::string(R"(
   ___  
  / _ \ 
 | | | |
 | |_| |
  \___/ 
)")),
        std::make_pair('p', std::string("asdf")),
        std::make_pair('q', std::string("asdf")),
        std::make_pair('r', std::string(R"(
  ____  
 |  _ \ 
 | |_) |
 |  _ < 
 |_| \_\
)")),
        std::make_pair('s', std::string("asdf")),
        std::make_pair('t', std::string("asdf")),
        std::make_pair('u', std::string("asdf")),
        std::make_pair('v', std::string("asdf")),
        std::make_pair('w', std::string("asdf")),
        std::make_pair('x', std::string("asdf")),
        std::make_pair('y', std::string(R"(
 __   __
 \ \ / /
  \ V / 
   | |  
   |_|  
)")),
        std::make_pair('z', std::string("asdf"))

    };

    std::string display(std::string& text);

};

This is taking a str reference in then looping over each character and finding that character in the map and getting its corresponding ASCII art letter then adding it to a final string.
This presents a problem. It prints each character on a new line when I want to print the string Art::display() returns.
What I have tried? I have tried removing the newlines but that jumbles bits and pieces together/apart.
What i want it to do? I want it to print a word left to right and not each char on a new line.

Ghasem Ramezani
  • 2,683
  • 1
  • 13
  • 32
noah
  • 107
  • 10
  • TIP: you could create a `art` directory and create a dedicate file for each latter and then `#include "art/A"` instead of using the art inside the file. It makes the code cleaner and easier to understand. – DadiBit Sep 09 '21 at 20:51
  • 1
    Perhaps raw strings aren't the right tool. What about an array of strings with each string representing a line? That way it's dead-easy to separate one line from the next and you can print `letters['a'][0] << letters['b'][0] << '\n' << letters['a'][1] << letters['b'][1] << '\n' << ...` – user4581301 Sep 09 '21 at 21:04
  • Iterate though all chars multiple times. The first time you print the first line of the character, the second time the second line, ect., so it would actually be easier to print the result, if the character data was stored each as `std::string[5]` – fabian Sep 09 '21 at 21:05

3 Answers3

2

For your output, keep an array of 5 lines, for the 5 distinct lines of the Art characters. When appending an art character, split it at the newlines, and append the first line of the art character to the first line of output, the second line of the art character to the second line of output, etc.


Meanwhile, you can use string_view instead of string for your map, and streamline your source by using UDLs and just using { } instead of make_pair.

        std::make_pair('a', std::string(R"(  
    _
   / \
  / _ \
 / ___ \
/__/ \__\
)")),

becomes:

        {'a', R"(  
    _
   / \
  / _ \
 / ___ \
/__/ \__\
)"sv },
JDługosz
  • 5,592
  • 3
  • 24
  • 45
2

I'd add a Letter class to simplify parsing and printing letters.

Example:

#include <iomanip>
#include <iostream>
#include <map>
#include <string_view>
#include <vector>

class Letter {
public:
    // The constructor takes a string_view and parses it into lines that
    // it stores in a vector<string_view>
    Letter(std::string_view l) : width_(0) {
        auto b = l.begin();
        for(auto it = b; it != l.end(); ++it) {
            if(*it == '\n') {
                auto w = it - b;
                if(w > width_) width_ = w;
                svs.emplace_back(b, w);
                b = it + 1;
            }
        }
        auto w = l.end() - b;
        if(w > width_) width_ = w;
        svs.emplace_back(b, w);
    }

    // some convenience functions
    unsigned width() const { return width_; }
    unsigned height() const { return svs.size(); }

    // An operator to get a specific line to render from this Letter.
    // It returns a string with the proper width (the max width of all the
    // lines for this Letter). If `line` is out of bounds, it returns a
    // blank line.    
    std::string operator()(unsigned line) const {
        if(line >= svs.size()) return std::string(width_, ' ');
        return std::string(svs[line]) + std::string(width_ - svs[line].size(), ' ');
    }

    // If you just want to print one letter:
    friend std::ostream& operator<<(std::ostream& os, const Letter& l) {
        for(auto& sv : l.svs) os << sv << '\n';
        return os;
    }

private:
    std::vector<std::string_view> svs;
    unsigned width_;
};

// A user defined literal operator to simplify creating the map:
Letter operator "" _L(const char* str, std::size_t len) {
    return std::string_view{str, len};
}

//---------------------------------------------------------------
std::map<char, Letter> letters = {
{' ', "   "_L }, // space
{'a',
R"(    _
   / \
  / _ \
 / ___ \
/__/ \__\
)"_L},
{'b',
R"( ____
| __ ) 
|  _ \ 
| |_) |
|____/ 
)"_L}
}; // note that each Letter ends with _L to trigger the user defined literal

// A printing function that can either be called with a specified height,
// or be made to find the appropriate height for this string.
void print(std::string_view txt, unsigned height = 0) {
    if(height == 0) {
        for(char ch : txt) {
            try {
                auto w = letters.at(ch).height();
                if(w > height) height = w;
            }
            catch(...) {} // don't mind non-existing letters
        }
    }
    for(unsigned i = 0; i < height; ++i) {
        for(char ch : txt) {
            const Letter& l = letters.at(ch);
            std::cout << l(i);
        }
        std::cout << '\n';
    }
}

int main() {
    print("abab baba");
}

Output:

    _     ____      _     ____      ____      _     ____      _    
   / \   | __ )    / \   | __ )    | __ )    / \   | __ )    / \   
  / _ \  |  _ \   / _ \  |  _ \    |  _ \   / _ \  |  _ \   / _ \  
 / ___ \ | |_) | / ___ \ | |_) |   | |_) | / ___ \ | |_) | / ___ \ 
/__/ \__\|____/ /__/ \__\|____/    |____/ /__/ \__\|____/ /__/ \__\

Note: MSVC (the compiler included with Visual Studio) had problems with my string_views for some reason. Here's a version that uses std::strings instead to make MSVC happy:

#include <iomanip>
#include <iostream>
#include <iterator>
#include <map>
#include <string>
#include <vector>

class Letter {
public:
    // The constructor takes a string_view and parses it into lines that
    // it stores in a vector<string_view>
    Letter() = default;
    Letter(const std::string& str) {
        auto b = str.cbegin();
        auto end = str.cend();
        for(std::string::const_iterator it = b; it != end; ++it) {
            if(*it == '\n') {
                std::size_t w = std::distance(b, it);
                if(w > width_) width_ = w;
                svs.emplace_back(b, it);
                b = it + 1;
            }
        }
        std::size_t w = std::distance(b, str.cend());
        if(w > width_) width_ = w;
        svs.emplace_back(b, str.cend());
    }

    // some convenience functions
    std::size_t width() const { return width_; }
    std::size_t height() const { return svs.size(); }

    // An operator to get a specific line to render from this Letter.
    // It returns a string with the proper width (the max width of all the
    // lines for this Letter). If `line` is out of bounds, it returns a
    // blank line.    
    std::string operator()(std::size_t line) const {
        if(line >= svs.size()) return std::string(width_, ' ');
        return svs[line] + std::string(width_ - svs[line].size(), ' ');
    }

    // If you just want to print one letter:
    friend std::ostream& operator<<(std::ostream& os, const Letter& l) {
        for(auto& sv : l.svs) os << sv << '\n';
        return os;
    }

private:
    std::vector<std::string> svs;
    std::size_t width_ = 0;
};

// A user defined literal operator to simplify creating the map:
Letter operator "" _L(const char* str, std::size_t len) {
    return std::string(str, str + len);
}

//---------------------------------------------------------------
std::map<char, Letter> letters = {
{' ', "   "_L },
{'a',
R"(    _
   / \
  / _ \
 / ___ \
/__/ \__\
)"_L},
{'b',
R"( ____
| __ ) 
|  _ \ 
| |_) |
|____/ 
)"_L}
}; // note that each Letter ends with _L to trigger the user defined literal

// A printing function that can either be called with a specified height,
// or be made to find the appropriate height for this string.
void print(std::string_view txt, std::size_t height = 0) {
    if(height == 0) {
        for(char ch : txt) {
            try {
                auto w = letters.at(ch).height();
                if(w > height) height = w;
            }
            catch(...) {} // don't mind non-existing letters
        }
    }
    for(std::size_t i = 0; i < height; ++i) {
        for(char ch : txt) {
            const Letter& l = letters.at(ch);
            std::cout << l(i);
        }
        std::cout << '\n';
    }
}

int main() {
    print("abab baba");
}
Ted Lyngmo
  • 93,841
  • 5
  • 60
  • 108
  • curious as to what c++ version youre using as using vs2019 with c++20 standards this doesnt compile and gives no overloaded function found for construct_at – noah Sep 11 '21 at 04:14
  • @noah MSVC is not the most conformat compiler around. The code I presented _should_ be accepted by a proper C++17 or C++20 compiler. – Ted Lyngmo Sep 11 '21 at 04:21
  • @noah [example](https://godbolt.org/z/d5YP17vz5) - `g++` and `clang++` does the right thing, `MSVC` does not. – Ted Lyngmo Sep 11 '21 at 04:32
  • thats interesting, ill have to make it work for msvc – noah Sep 11 '21 at 04:38
  • @noah Yeah, I didn't expect MSVC to fail on this actually. If you don't get it to work in a few hours, just ping here and I'll add a version that MSVC understands. – Ted Lyngmo Sep 11 '21 at 04:42
  • it looks like one compile error stems from another, leaving only two actual errors, one being no overloaded function for construct_at and cannot convert std::string_view to const char*. The problem im having is the fact it doesnt show which line it comes from and instead the function(s) inside xmemory.h, 710 being the first overload error and line 714 being the 2nd error – noah Sep 11 '21 at 04:58
  • @noah Yes, I noticed. I added a version that uses `std::string`s instead. – Ted Lyngmo Sep 11 '21 at 12:07
1

In the console you are writing line by line typically. When you print one of the characters, you take the string, and each line with its \n will be printed. Line by line.

The fact that each character is printed one after the other, is just normal, this is how outputing works: line by line.

The jumbing is just the lines been appended to the same line. For the charachter a, instead of having X lines to draw the character, they all get merged into one line. Which is not what you want, but you were edging towards the right idea I think.

What you need to do is for all the required characters, merge them line-wise, and output the result line by line as you are already doing, but instead of one character at a time, it's with the merge of all the needed characters.

I'm adding a "visual" to hopefully make things clearer.

Currently you have

    _\n
   / \\n
  / _ \\n
 / ___ \\n
/__/ \__\\n
  ____\n
 | __ ) \n
 |  _ \ \n
 | |_) |\n
 |____/ \n

The jumbing is caused because removing the \n does this (for a):

_/ \/ _ \/ ___ \/__/ \__\

What you want is

    _         ____ \n
   / \       | __ ) \n
  / _ \      |  _ \ \n
 / ___ \     | |_) | \n
/__/ \__\    |____/ \n
ShadowMitia
  • 2,411
  • 1
  • 19
  • 24
  • so each character needs to be the same length line wise? – noah Sep 09 '21 at 21:06
  • @noah You don't NEED to but in your case it would simplify things. Otherwise it's just a matter of finding the longest substring in a character, and then adding paddings when needed. But in your case making sure all the `\n` align, would make things easier for you. – ShadowMitia Sep 09 '21 at 21:11