How can I transform a string into an abbreviated form?
Asked Answered
R

6

6

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.

Runagate answered 27/5, 2009 at 5:4 Comment(0)
B
8

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;
}
Barbur answered 27/5, 2009 at 5:9 Comment(2)
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 :)Runagate
Awesome, complete, helpful answer.Mayer
E
2

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 "...".
Expressive answered 27/5, 2009 at 5:8 Comment(0)
G
2

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.

Gynecology answered 27/5, 2009 at 5:13 Comment(0)
I
1
  • 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...

Impeccant answered 27/5, 2009 at 5:39 Comment(2)
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)Barbur
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).Impeccant
R
1

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;
  */
}
Runagate answered 28/5, 2009 at 8:59 Comment(2)
+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.Barbur
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 :)Runagate
A
1

If you just want the "standard" ellipsis format, ("Hello...") you can use the DrawText (or the equivalient MFC function) function passing DT_END_ELLIPSIS.

Check out also DT_WORD_ELLIPSIS (it truncates any word that does not fit in the rectangle and adds ellipses) or DT_PATH_ELLIPSIS (what Explorer does to display long paths).

Adroit answered 31/3, 2017 at 6:42 Comment(3)
I think I needed an algorithm when I posted the question, but thanks anyway, that's super convenient!Runagate
I found your question looking for the same problem, looked up on MSDN GetTextExtent, and found this simple option in DrawText. Since I needed ellipsis at the end, for me it was enough, and maybe for some future readers will be enough too!Monoploid
yes, that's the way to do it if we use GDI and need basic text trimming.Runagate

© 2022 - 2024 — McMap. All rights reserved.