views:

413

answers:

5

I want to fit strings into a specific width. Example, "Hello world" -> "...world", "Hello...", "He...rld".

Do you know where I can find code for that? It's a neat trick, very useful for representing information, and I'd like to add it in my applications (of course).

Edit: Sorry, I forgot to mention the Font part. Not just for fixed width strings but according to the font face.

+2  A: 

It's really pretty trivial; I don't think you'll find specific code unless you have something more structured in mind.

You basically want:

  1. to get the length of the string you have, and the window width.
  2. figure out how many charaters you can take from the original string, which will basically be window width-3. Call that k.
  3. Depending on whether you want to put the ellipsis in the middle or at the right hand end, either take the first floor(k/2) characters from one end, concatenated with "...", then concatenated with the last floor(k/2) characters (with possibly one more character needed because of the division); or take the first k characters, ollowed by "...".
Charlie Martin
+7  A: 

It's a pretty simple algorithm to write yourself if you can't find it anywhere - the pseudocode would be something like:

if theString.Length > desiredWidth:
    theString = theString.Left(desiredWidth-3) + "...";

or if you want the ellipsis at the start of the string, that second line would be:

    theString = "..." + theString.Right(desiredWidth-3);

or if you want it in the middle:

    theString = theString.Left((desiredWidth-3)/2) + "..." + theString.Right((desiredWidth-3)/2 + ((desiredWidth-3) mod 2))

Edit:
I'll assume you're using MFC. Since you're wanting it with fonts, you could use the CDC::GetOutputTextExtent function. Try:

CString fullString
CSize size = pDC->GetOutputTextExtent(fullString);
bool isTooWide = size.cx > desiredWidth;

If that's too big, then you can then do a search to try and find the longest string you can fit; and it could be as clever a search as you want - for instance, you could just try "Hello Worl..." and then "Hello Wor..." and then "Hello Wo..."; removing one character until you find it fits. Alternatively, you could do a binary search - try "Hello Worl..." - if that doesn't work, then just use half the characters of the original text: "Hello..." - if that fits, try halfway between it and : "Hello Wo..." until you find the longest that does still fit. Or you could try some estimating heuristic (divide the total length by the desired length, proportionately estimate the required number of characters, and search from there.

The simple solution is something like:

unsigned int numberOfCharsToUse = fullString.GetLength();
bool isTooWide = true;
CString ellipsis = "...";
while (isTooWide)
{
    numberOfCharsToUse--;
    CString string = fullString.Left(numberOfCharsToUse) + ellipsis;
    CSize size = pDC->GetOutputTextExtent(string);
    isTooWide = size.cx > desiredWidth;
}
Smashery
Yes, its MFC and I am going to implement a *clever* fit. I wanted to see if there is an efficient implementation for that kind of stuff before trying to implement my own. But I guess I have to sit down and be creative :)
Nick D
Awesome, complete, helpful answer.
Aidan Ryan
Thanks, Aidan :-)
Smashery
+2  A: 

I think Smashery's answer is a good start. One way to get to the end result would be to write some test code with some test inputs and desired outputs. Once you have a good set of tests setup, you can implement your string manipulation code until you get all of your tests to pass.

Andy White
+1  A: 
  • Calculate the width of the text ( based on the font)

In case you are using MFC the API GetOutputTextExtent will get you the value.

  • if the width exceeds the given specific width, calculate the ellipse width first:

    ellipseWidth = calculate the width of (...)

  • Remove the string part with width ellipseWidth from the end and append ellipse.

    something like: Hello...

aJ
You will probably find, though, that width(text) + width(ellipsis) does not equal width(text+ellipsis). This is because some glyphs overlap when they are rendered to make it look prettier. So you can't just subtract the ellipsewidth from the desired width - you have to check the full length (including ellipsis)
Smashery
Yes, thats correct. In a loop we need to check whether the size with ellipse corresponds to given size (in order to identify the characters to remove from string).
aJ
+1  A: 

For those who are interested for a complete routine, this is my answer :

/**
 *  Returns a string abbreviation
 *  example: "hello world" -> "...orld" or "hell..." or "he...rd" or "h...rld"
 *
 *  style:
      0: clip left
      1: clip right
      2: clip middle
      3: pretty middle
 */
CString*
strabbr(
  CDC* pdc,
  const char* s,
  const int area_width,
  int style  )
{
  if (  !pdc || !s || !*s  ) return new CString;

  int len = strlen(s);
  if (  pdc->GetTextExtent(s, len).cx <= area_width  ) return new CString(s);

  int dots_width = pdc->GetTextExtent("...", 3).cx;
  if (  dots_width >= area_width  ) return new CString;

  // My algorithm uses 'left' and 'right' parts of the string, by turns.
  int n = len;
  int m = 1;
  int n_width = 0;
  int m_width = 0;
  int tmpwidth;
  // fromleft indicates where the clip is done so I can 'get' chars from the other part
  bool  fromleft = (style == 3  &&  n % 2 == 0)? false : (style > 0);
  while (  TRUE  ) {
    if (  n_width + m_width + dots_width > area_width  ) break;
    if (  n <= m  ) break; // keep my sanity check (WTF), it should never happen 'cause of the above line

    //  Here are extra 'swap turn' conditions
    if (  style == 3  &&  (!(n & 1))  )
      fromleft = (!fromleft);
    else if (  style < 2  )
      fromleft = (!fromleft); // (1)'disables' turn swapping for styles 0, 1

    if (  fromleft  ) {
      pdc->GetCharWidth(*(s+n-1), *(s+n-1), &tmpwidth);
      n_width += tmpwidth;
      n--;
    }
    else {
      pdc->GetCharWidth(*(s+m-1), *(s+m-1), &tmpwidth);
      m_width += tmpwidth;
      m++;
    }

    fromleft = (!fromleft); // (1)
  }

  if ( fromleft ) m--; else n++;

  // Final steps
  // 1. CString version
  CString*  abbr = new CString;
  abbr->Format("%*.*s...%*.*s", m-1, m-1, s, len-n, len-n, s + n);
  return abbr;

  /* 2. char* version, if you dont want to use CString (efficiency), replace CString with char*,
                       new CString with _strdup("") and use this code for the final steps:

  char* abbr = (char*)malloc(m + (len-n) + 3 +1);
  strncpy(abbr, s, m-1);
  strcpy(abbr + (m-1), "...");
  strncpy(abbr+ (m-1) + 3, s + n, len-n);
  abbr[(m-1) + (len-n) + 3] = 0;
  return abbr;
  */
}
Nick D
+1 Nice work! Just a word of caution: I discovered when doing a similar thing in another language that when you combine letters in a font, they may not necessarily equal the sum of their individual lengths. For instance, when you put 'W' next to 'A', the font renderer may put them a bit closer together to make it look prettier to the human eye. So width('W') + width('A') may not equal width('WA'). For small strings, it will probably only make the difference of a few pixels, but for longer ones, it may be quite noticeable.
Smashery
Thanks. Ok, I'll check it out.
Nick D
Also thanks for suggesting binary search in your answer, very good idea! Maybe I'll implement it when I have time - binary search can be tricky so I skipped it in the first version :)
Nick D