VFP function to get the first line of a piece of text
This is the story of a useful little utility function to extract the “first line” out of a block of multi-line text. If all you want is the function, then you can stop on the first page. If you’re interested in how someone can spend an hour writing a simple function that is less than 25 lines long, and why it ended up how it is, then read to the gory end!
function GetFirstLine( tcText [, tnMaxLen] [, tcEllipses] )
I had a table with a multi-line text field, and needed sometimes to display or use only the first line so it could be shown in a limited space (a grid for example). If the first line wasn’t the whole of the field value, I also needed it to display a trailing “…” to indicate there was more that wasn’t shown. Also, this function needed to be quite fast - I was planning it would appear as the ControlSource expression of a grid column, for example. Something like this:

As a definition of “first line” I decided that I wanted the output of the function to be no greater than 75 characters, and contain only text up to the first hard line break in the text. I wasn’t too worried about word wrap, although that would be cute.
Deciding how to implement this function, I could think of three ways:
- Use at(chr(13),… to get the first line, then trim further if it was greater than 75 characters
- Use alines(…) and take only the first element of the array
- use mline(..) function, saving and restoring the value of SET MEMOWIDTH
My initial instinct told me that using mline() would be the easiest, since it already had support for specifying the maximum length of a line, and would automatically break on a word boundary - which would require additional code with either of the other two. However because of the need to save and restore SET MEMOWIDTH, I shied away from this in favour of the other two. Alines() I thought would be the fastest to code, but since I only needed the first line, it seemed a bit over the top - couldn’t I get the first line with LEFT() or STREXTRACT() or something? However there was a complication here - I couldn’t be sure that a line break would be represented by a chr(13) - it could just as validly be chr(10). That meant additional code to handle the extra case.
What I should have done at this point is followed the golden rule of optimization: Don’t optimize. Ever!. I’d have picked the mline version, on the basis that this was easiest to write, and worry about speed only if the users complained. I’d have been done in five minutes.
My curiosity, however, was aroused. So I spent a good half hour or more writing three functions using the three techniques, and ran some speed tests with different sized input strings. The results were probably predictable:
- The AT(CHR(13)…) version was the most complicated code, but was fastest on everything but an empty string. On multi-line input, it beat the pants of the other two. However .. it had bugs.
- Of the other two, the ALINES() version was was faster on single-line input, but got progressively slow the more lines there were
- By comparison, MLINE() was slowest of all on single line input, but on two lines or more its time was constant (programming theory calls this “O(1)”)
- On very large input strings (eg 5000 characters or so) all the functions got slower - ALINES() extremely so
The MLINES() version is the winner here - despite the extra SET MEMOWIDTH handling the code was simple and easily understood, and it worked flawlessly the first time, with acceptable speed.
However I knew that the vast majority of input to this function would be empty strings, or just a few words, so there was an easy optimization I could do without looking stupid.
If the input was single-line and less than 75 characters, then the entire string could just be returned untouched. This would cover off empty and short input. If the input string was longer than 75 characters or contained a line break, then use the MLINES() technique.
With a bit of extra code to make this a bit more flexible and guard against error conditions, here is what I came up with:
function GetFirstLine( tcMultilineText as string, tnMaxLen as Integer, tcEllipses as string ) * String must be null or character assert vartype(m.tcMultilineText)=='C' or vartype(m.tcMultilineText)=='X' tnMaxLen = evl(m.tnMaxLen,75) && Arbitary size * Optimize out the case of a short string if isnull(m.tcMultilineText) ; or (len(m.tcMultilineText)< =m.tnMaxLen ; and (not chr(13)$m.tcMultilineText) ; and (not chr(10)$m.tcMultilineText) ) return m.tcMultilineText else * extract the first line local lnSaveMemoWidth, lnSaveMLine, lcReturn lnSaveMLine = _mline lnSaveMemoWidth = set("Memowidth") tcEllipses = iif(vartype(m.tcEllipses)=='C',m.tcEllipses,'..') set memowidth to m.tnMaxLen lcReturn = mline(m.tcMultiLineText,1) _mline = m.lnSaveMLine set memowidth to m.lnSaveMemoWidth return left(m.lcReturn,m.tnMaxLen-len(m.tcEllipses))+m.tcEllipses endif endfunc && GetFirstLine
For the gory details of timing tests, read the next page.