Determining the Width of a String
Ben Halpern Sep 7 '16
I am no expert in word arrangement aesthetics, but I think it's ugly when a body of text, especially a centered one, has one word on the last line. This was happening on this website. Like a good developer, instead of solving this one issue at the point, I decided to modify a core class in the language I am working to solve a bunch of hypothetical future issues. 😋
There are a few ways to solve this problem. Here is a Stack Overflow question about it. However, I have a constraint on this application where, for render performance reasons, I do not want to do calculations on the client side for formatting purposes. So I set off to solve this problem without having to calculate on the fly. The fundamental problem here is that the server-side application does not have any awareness of the width of a piece of text. We typically only pay attention to the number of characters in a string, not the actual width of it.
The following strings all have ten characters
Here they are again, using a monospace font
"MMMMMMMMMM" "mmmmmmmmmm" "FFFFFFFFFF" "ffffffffff" "AAAAAAAAAA" "aaaaaaaaaa"
Obviously any situation where precision is even remotely important, we cannot use the number of characters, the length, of a string as a proxy for its width. And once we know the width as a precise value we can calculate on the server, we can use it in a variety of interesting ways, such as validation on input lengths. Usernames typically have character limits, and a big reason for this is to ensure we can sanely render their name on a page, but if we are confident we can accurately determine width, we can consider it when performing validations.
Because I am using Ruby on the backend, it only makes sense that I would want to monkey patch the String class in order to accomplish this for maximum expressiveness. It just seems in the spirit of the language, creating a nice expressive way to refer to a string's width. It seems that
"hello".width belongs in the same category of
"hello".length, or its alias
In Ruby, this is remarkably simple.
class String def width(font="Helvetica") StringWidth.new.calc(self,font) end end
I made a second class called
StringWidth because it made sense to just give string only one new method. I am not sure what the truly most "Ruby" way to do this would be.
String#width accepts an optional font and returns the normalized length of the string. Each character is given a value where 1.0 is the approximate mean. The "f" character in Helvetica, as seen above, gets a score of 0.559, to denote its narrowness. The "typical" string of five characters will respond with a value of about 5.0. An especially narrow string may return 2.5 and a wide one could return, perhaps, 8.0. This made sense to me, but I'd be open to hearing about other ideas about how this should behave.
In order to calculate the width of the string in Ruby, there are a few options. Here is a Stack Overflow question on the topic.
the_text = "TheTextYouWantTheWidthOf" label = Draw.new label.font = "Vera" label.text_antialias(true) label.font_style=Magick::NormalStyle label.font_weight=Magick::BoldWeight label.gravity=Magick::CenterGravity label.text(0,0,the_text) metrics = label.get_type_metrics(the_text) width = metrics.width height = metrics.height
This depends on RMagick. I personally do not enjoy working with Ruby's general image manipulation libraries and their hoards of dependencies, so I decided against this from the get. A core premise of the whole thing is that the calculations should not have to be made at runtime anyway, they can easily be pre-calculated and then stored as a dictionary. This is more than suitable for my purposes.
I also had the thought that doing the initial calculations on the client, relying on the actual browser renderings of the fonts, would be the most accurate way to do populate the dictionary. This also allows for additionally complicated scenarios like letter spacing to be accounted for. I have not yet made this part of the API, but it could easily be extended to account for letter spacing, etc, as that is needed. So I set up a script that printed each character I wanted to include in the library 100 times and then took the width and normalized it. I took the result, printed it out as JSON and then copied it into my Ruby code. I can re-run this script any time the functionality needs to be updated and I am happy with this approach.
So there you have it. The Ruby codebase that runs this web application now has a string implementation with width awareness.
Doesn't this look better?
In the template, I used some logic to ensure my basic case turns out looking a bit better, but thus far I have only accounted for one basic case, where I modify the title's class based on whether the string is in one specific "awkward zone". I have not yet accounted for the possible variations in terms of title widths and window widths. But now that I can easily refer to the string's true width as a reliable value, adjusting the rendering for the different scenarios will be a cinch.