Created
January 13, 2023 20:04
-
-
Save grorg/a2ff323d3aa059b4a4fb194bde8e40d6 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// TextMeasurement.cpp | |
// | |
// Originally taken from Cameron McCormack's text_measurement.cpp | |
// https://gist.github.com/heycam/942f621ff9def88af92ddc3ec2d59a03 | |
// | |
// ACME Browser Corporation is writing a brand new browser engine! One of the | |
// main responsibilities of a browser is to display text to the user, and this | |
// text must be wrapped to the width of the browser window so that the user | |
// is not scrolling back and forth all the time. | |
// | |
// One of ACME's junior engineers has taken a first attempt at writing some text | |
// measurement and wrapping functionality. The two main functions below are | |
// measureText and wrapText. They've written some tests, and it looks like | |
// the code is working! But they would appreciate review of their code. | |
// | |
// Please take a read through the code, compile and run it, and try to get an | |
// understanding of how it works. During the interview, we'll have a | |
// (Web-based) live coding session to refactor and improve the code. | |
// | |
// This should compile with any C++17 compiler, including various | |
// online tools (such as https://godbolt.org). | |
// | |
// For example, on macOS, with Xcode or the command-line tools installed: | |
// $ clang++ -std=c++17 -o TextMeasurement TextMeasurement.cpp | |
#include <cassert> | |
#include <string> | |
#include <vector> | |
#include <cstring> | |
#include <iostream> | |
#include <iomanip> | |
using namespace std; | |
// ---- Main text measurement code ---- | |
// A single character in a Font and its width. | |
struct Character { | |
char character; // 0 means the default character! | |
float width; | |
}; | |
// A Font is a list of characters and their widths. | |
class Font { | |
public: | |
Font(const char* fontName) { | |
assert(fontName != nullptr); | |
name = fontName; | |
m_count = 0; | |
} | |
void setDefaultCharacterWidth(float w); | |
void setCharacterWidth(char c, float w); | |
void setCharacterWidths(const char* s, float w); | |
int getCount() { return m_count; } | |
Character get(int i) { return m_characters[i]; } | |
std::string name; | |
private: | |
Character* m_characters; | |
int m_count; | |
}; | |
void Font::setDefaultCharacterWidth(float w) { | |
if (m_count == 0) { | |
// can't have more than 256 char values | |
m_characters = new Character[256]; | |
} | |
m_characters[m_count].character = 0; | |
m_characters[m_count].width = w; | |
m_count++; | |
} | |
void Font::setCharacterWidth(char c, float w) { | |
if (m_count == 0) | |
{ | |
// can't have more than 256 char values | |
m_characters = new Character[256]; | |
} | |
m_characters[m_count].character = c; | |
m_characters[m_count].width = w; | |
m_count++; | |
} | |
void Font::setCharacterWidths(const char* s, float w) { | |
for ( int i = 0; i < strlen(s); i++) { | |
setCharacterWidth(s[i], w); | |
} | |
} | |
float measureText(Font f, std::string s) { | |
if (s.length() == 0) { | |
return 0; | |
} | |
float total_width = 0; | |
for (int i = 0; i < s.length(); i++) { | |
float w; | |
bool found = false; | |
for (int j = 0; j < f.getCount(); j++) { | |
if (f.get(j).character == s[i]) { | |
w = f.get(j).width; | |
found = true; | |
} | |
} | |
if (!found) { | |
// look up the default width | |
for (int j = 0; j < f.getCount(); j++) { | |
if (f.get(j).character == 0) { | |
w = f.get(j).width; | |
} | |
} | |
} | |
total_width = total_width + w; | |
} | |
return total_width; | |
} | |
vector<string> wrapText(Font f, std::string s, float lineWidth) { | |
vector<string> result; | |
int lineStart = 0; | |
int lineEnd = 0; | |
while (true) { | |
float w = measureText(f, s.substr(lineStart, lineEnd - lineStart)); | |
if (w > lineWidth) { | |
// too many words on the line. go back to find the last space. | |
while (s[lineEnd - 1] != ' ') { | |
lineEnd = lineEnd - 1; | |
} | |
lineEnd = lineEnd - 1; // remove the space | |
result.push_back(s.substr(lineStart, lineEnd - lineStart)); | |
lineEnd++; // but don't include the space on the next line | |
lineStart = lineEnd; | |
} else { | |
lineEnd = lineEnd + 1; | |
} | |
if (lineEnd >= s.length()) { | |
// the final line | |
result.push_back(s.substr(lineStart, s.length() - lineStart)); | |
break; | |
} | |
} | |
return result; | |
} | |
// ---- Testing framework ---- | |
std::ostream& operator<<(std::ostream& os, const std::vector<string>& a) { | |
os << '['; | |
for (int i = 0; i < a.size(); i++) { | |
if (i > 0) { | |
os << ", "; | |
} | |
os << '\'' << a[i] << '\''; | |
} | |
return os << ']'; | |
} | |
void testMeasureText(const Font& f, std::string s, float expected) { | |
float actual = measureText(f, s); | |
bool pass = actual == expected; | |
cout << (pass ? "PASS" : "FAIL") | |
<< ": measureText(" << f.name << ", \"" << s << "\") -- got " | |
<< actual; | |
if (!pass) { | |
cout << ", expected " << expected; | |
} | |
cout << '\n'; | |
} | |
void testWrapText(const Font& f, std::string s, float lineWidth, std::vector<string> expected) { | |
std::vector<string> actual = wrapText(f, s, lineWidth); | |
bool pass = actual == expected; | |
cout << (pass ? "PASS" : "FAIL") | |
<< ": wrapText(" << f.name << ", \"" << s << "\", " << lineWidth | |
<< ") -- got " << actual; | |
if (!pass) { | |
cout << ", expected " << expected; | |
} | |
cout << '\n'; | |
} | |
// ---- Tests ---- | |
void runTests() { | |
cout << "Running tests...\n\n"; | |
// All characters in this font are 10px wide. | |
Font mono("Monospace"); | |
mono.setDefaultCharacterWidth(10); | |
// Most characters in this font are 8px wide, but they can range from | |
// 4px to 14px. | |
Font prop("Proportional"); | |
prop.setCharacterWidths(" !'(),-.:;IJ[]fijl", 4); | |
prop.setCharacterWidths("\"/?\\_crstz{}", 6); | |
prop.setCharacterWidths("#&ABCDGHNU", 10); | |
prop.setCharacterWidths("%@MOQw", 12); | |
prop.setCharacterWidths("Wm", 14); | |
prop.setDefaultCharacterWidth(8); | |
// Tests. | |
testMeasureText(mono, "Some text", 9 * 10); | |
testMeasureText(prop, "Some text", 8 + 8 + 14 + 8 + 4 + 6 + 8 + 8 + 6); | |
testWrapText(mono, "Some text to wrap", 100, { "Some text", "to wrap" }); | |
testWrapText(prop, "Some text to wrap", 100, { "Some text to", "wrap" }); | |
cout << "\nFinished.\n"; | |
} | |
int main() { | |
runTests(); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment