25
\$\begingroup\$

Monday Mini-Golf: A series of short challenges, posted (hopefully!) every Monday.

Many web applications (especially social media) automatically truncate long passages of text so they fit within the app's formatting. In this challenge, we're going to create an algorithm to automatically trim a passage of text to a certain length.

Challenge

The goal of the challenge is to write a program or function that takes in two arguments:

  • T, the text to truncate.
  • L, the maximum length to return.

And returns T, truncated with the following logic:

  • If the length of T is less than or equal to L, no truncation is needed. Return the original string.
  • Truncate T to length L-2. If this contains no spaces or hyphens, return T truncated to exactly L-3 characters, followed by an ellipsis ....
  • Otherwise, trim the end of the result up to the last space or hyphen. Add an ellipsis ... and return the result.

Details

  • T and L may be taken in either order and any format.
  • You may assume that 3 < L < 231.
  • You may not use U+2026 Horizontal Ellipsis ; you must use three periods.
  • The input will not start with a space or a hyphen.
  • The input will not contain any whitespace other than regular spaces. (No tabs, newlines, etc.)

Test-cases

Inputs:

"This is some very long text." 25
"This-is-some-long-hyphen-separated-text." 33
"Programming Puzzles & Code Golf is a question and answer site for programming puzzle enthusiasts and code golfers." 55 
"abcdefghijklmnopqrstuvwxyz" 20
"a b c" 4
"Very long." 100

Outputs:

"This is some very long..."
"This-is-some-long-hyphen..."
"Programming Puzzles & Code Golf is a question and..."
"abcdefghijklmnopq..."
"a..."
"Very long."

(Note that the quotes are just to specify that these are strings; they need not be included.)

Scoring

This is , so shortest valid code in bytes wins. Tiebreaker goes to submission that reached its final byte count first. The winner will be chosen next Monday, Oct 5. Good luck!

Edit: Congrats to your winner, @Jakube with Pyth again, with 25 bytes!

\$\endgroup\$
  • 7
    \$\begingroup\$ Answers to this challenge should be a standard feature in their respective languages. Too often I have seen UI that feature bad trunca... \$\endgroup\$ – Sanchises Sep 28 '15 at 21:20
  • 1
    \$\begingroup\$ ... "Otherwise, trim the end of the result up to and " NOT " including the last space or hyphen." Right? \$\endgroup\$ – anatolyg Sep 29 '15 at 9:01
  • \$\begingroup\$ Will the text have any tabs? \$\endgroup\$ – kirbyfan64sos Sep 29 '15 at 16:52
  • \$\begingroup\$ @anatolyg No, because then the final space or hyphen would appear before the ellipsis. \$\endgroup\$ – ETHproductions Sep 29 '15 at 17:38
  • \$\begingroup\$ @kirbyfan64sos Nope. I'll add that to the Details section. \$\endgroup\$ – ETHproductions Sep 29 '15 at 17:39

21 Answers 21

12
\$\begingroup\$

Pyth, 25 bytes

+WnzK<zeo}@zN" -"-Q2K"...

Try it online: Demonstration or Test Suite

Explanation:

+WnzK<zeo}@zN" -"-Q2K"...  implicit: z = input string, Q = input number
        o        -Q2       order the indices N in [0, 1, ..., Q-3] by
         }@zN" -"            z[T] in " -"
                           (hyphen-indices get sorted to the back)
       e                   take the last such number
     <z                    reduce z to length ^
    K                      save this string to K
+WnzK               K"...  print (K + "...") if z != K else only K
\$\endgroup\$
  • 4
    \$\begingroup\$ I like the way the code trails off at the end... \$\endgroup\$ – mathmandan Sep 29 '15 at 15:19
7
\$\begingroup\$

Perl, 69 59 52 bytes

51 bytes code + 1 byte command line. Assumes numerical input is allowed to be given with -i parameter.

s/.{$^I}\K.*//&&s/(^([^ -]*).|.*\K[ -].*)..$/$2.../

Usage:

echo "This-is-some-long-hyphen-separated-text." | perl -p -i"33" entry.pl
\$\endgroup\$
7
\$\begingroup\$

Python 2, 78 73 bytes

t,l=input()
u=t[:l-2]
print(t,u[:max(map(u.rfind,' -'))]+'...')[l<len(t)]

Input format follows the example input.

\$\endgroup\$
  • 1
    \$\begingroup\$ A familiar name from Anarchy Golf. Welcome! \$\endgroup\$ – xnor Sep 29 '15 at 7:07
7
\$\begingroup\$

JavaScript (ES6), 123 78 67 61 bytes

I didn't expect to be able to cut this down so much, but it turns out the splice/replace combo is able to cover every case where truncation is needed.

(T,L)=>T[L]?T.slice(0,L-2).replace(/([ -][^ -]*|.)$/,'...'):T

First argument is the string, second is the length. Special thanks to edc65 for the length check optimization!

Here's the original code (123 bytes):

(T,L)=>(T.length>L?(S=T.slice(0,L)).slice(0,(m=Math.max(S.lastIndexOf` `,S.lastIndexOf`-`))<0?L-3:Math.min(L-3,m))+'...':T)
\$\endgroup\$
  • 4
    \$\begingroup\$ Clever! +1. Tip: often you don't need .length to check the length of a string (T,L)=>T[L]?T.slice(0,L-2).replace(/([ -][^ -]*|.)$/,'...'):T score 61 \$\endgroup\$ – edc65 Sep 28 '15 at 22:11
  • \$\begingroup\$ @edc65 Doh! I had been looking for an optimization on the length check, thinking there had to be some way to trim that down, but your method hadn't occurred to me. Excellent suggestion! :D \$\endgroup\$ – Mwr247 Sep 29 '15 at 14:49
  • \$\begingroup\$ You can replace [ -][^ -] with \s\S to save 5 more bytes \$\endgroup\$ – Shaun H Sep 29 '15 at 20:49
  • \$\begingroup\$ Great solution! @ShaunH, if he does that it won't work for the hyphens, surely? \$\endgroup\$ – Jarmex Sep 29 '15 at 23:03
  • \$\begingroup\$ @Jarmex Silly brain, yeah it most definitely does not. \$\endgroup\$ – Shaun H Sep 30 '15 at 12:11
5
\$\begingroup\$

TI-BASIC, 87 bytes

Prompt L,Str1
For(X,1,L
{inString(Str1," ",X),inString(Str1,"-",X
max(I,max(Ans*(Ans≤L-3->I
End
Str1
If L<length(Ans
sub(Ans,1,I+(L-3)not(I))+"...
Ans

TI-BASIC doesn't have many string manipulation commands, so we need to find the last index manually: if the string doesn't contain the string to search for, inString( returns 0. We search for hyphens and spaces starting at every position from 1 to L, and record the greatest number less than or equal to L-3. If that number I is still 0, we use L-3 as the ending index instead.

Due to the calculator's limitations, the largest addressable index of a string is 9999; therefore, this will fail for larger strings.

I rely on the calculator's behavior of automatically initializing the variable I to 0, so delete I or clear your calculator's memory before running.

\$\endgroup\$
  • \$\begingroup\$ There's a shorter solution using lists to find the greatest index, but then the size limit would be ~500, not 9999. \$\endgroup\$ – lirtosiast Sep 29 '15 at 4:58
4
\$\begingroup\$

C# .NET, 187 169 bytes

Hmm...

string f(string T,int L){if(T.Length<=L)return T;T=T.Substring(0,L-2);return T.Substring(0,T.Contains(" ")||T.Contains("-")?T.LastIndexOfAny(new[]{' ','-'}):L-3)+"...";}
\$\endgroup\$
  • \$\begingroup\$ yes of course, I just removed spaces to decrease bytes. \$\endgroup\$ – Salah Alami Sep 28 '15 at 22:02
3
\$\begingroup\$

Python 2, 105 bytes

def t(s,l):a=s[:l-2];return s[:max(a.rfind(' '),a.rfind('-'))]+'...'if' 'in a or'-'in a else a[:-1]+'...'

Called with

>>> print t("This is some very long text.", 25)
This is some very long...
\$\endgroup\$
1
\$\begingroup\$

Groovy, 95 bytes

a={T,L->F=T.size()<=L?T:T[0..L-3]
m=F=~'(.*[- ])'
F==T?F:m?m[0][0].trim()+'...':F[0..-2]+'...'}

Pretty straightforward, can probably be golfed further

\$\endgroup\$
1
\$\begingroup\$

CJam, 34 bytes

q~1$<_@={-2<_W%{" -"&}#~We|<'.3*}|

Try it online: Chrome | Firefox

\$\endgroup\$
1
\$\begingroup\$

T-SQL,145 bytes

create proc a(@t varchar(max),@l int)as if LEN(@t)<=@l return @t;set @t = LEFT(@t,@l-3) select LEFT(@t,LEN(@t)-CHARINDEX('-',REVERSE(@t)))+'...'

usage:

exec a("This is some very long text.", 25) exec a("This-is-some-long-hyphen-separated-text.", 33)

\$\endgroup\$
1
\$\begingroup\$

rs, 116

(\d+)$/(_)^^(\1)
___$/
\t
+\t(.)(.*) (_)(_*)$/\1\t\2 \4
\t(.*?)_+$/\1\t
\t( ?).+/\1
[- ][^- \t]*$/
(?<!\t)$/...
 \t/

At least it's shorter than C#...

Live demo and test cases.

\$\endgroup\$
1
\$\begingroup\$

Ceylon 386 333 252 230 222 216 171 153 131 111

String t(String s,Integer l)=>s.size<l then s else s[0:(s[0:l-2].lastIndexWhere(" -".contains)else l-3)]+"...";

Ungolfed Original:

String truncate(String text, Integer length) {
    if(text.size < length) {
        return text;
    }
    Boolean spacePredicate(Character char) {
        return char == ' ' || char == '-';
    }
    Integer? spaceIndex = text[0:length-2].lastIndexWhere(spacePredicate);
    if(exists spaceIndex) {
        return text[0:spaceIndex] + "...";
    }
    return text[0:length-3]+"...";
}

This is 386 bytes/characters. Some interesting features here:

The x[y:z] syntax is syntactic sugar for x.measure(y, z), and returns a subrange of x starting at y with length z – for strings, this is a substring. (There is also x[y..z] syntax, which is a span from index y to z, both inclusive, as well as half-open spans x[...z] and x[y...].)

List.lastIndexWhere takes a predicate (i.e. a function taking a list element and returning a boolean, i.e. here a Callable<Boolean, [Character]>), and gives the index of the last list element where the predicate is fulfilled (or null, if it's never fulfilled). As strings are lists, this works for strings too.

The result of this, spaceIndex is of type Integer|Null, or Integer? for short – i.e. it can either be an Integer or null (the only value of type Null). (The name spaceIndex comes from when I didn't realize that - was also special – I guess breakIndex would be better.)

With exists spaceIndex we can check if spaceIndex is non-null, and do something different then. (Inside this if-block the compiler knows that it is non-null ... without that it would have complained if I used spaceIndex to access the string.)

Instead of the local function spacePredicate we can also use an anonymous function

(Character char) => char == ' ' || char == '-'

This brings us to 333 characters:

String truncate(String text, Integer length) {
    if(text.size < length) {
        return text;
    }
    Integer? spaceIndex = text[0:length-2].lastIndexWhere(
        (Character char) => char == ' ' || char == '-');
    if(exists spaceIndex) {
        return text[0:spaceIndex] + "...";
    }
    return text[0:length-3]+"...";
}

Next optimization is to use shorter variable and function names, which brings us down by 81 bytes to 252:

String t(String s, Integer l) {
    if(s.size < l) {
        return s;
    }
    Integer? i = s[0:l-2].lastIndexWhere(
        (Character e) => e == ' ' || e == '-');
    if(exists i) {
        return s[0:i] + "...";
    }
    return s[0:l-3]+"...";
}

The predicate function actually doesn't need its argument type declared, that can be inferred by the compiler. Same for the type of i (where we still have to write value to mark it as a declaration). Now that declaration is short enough to fit on one line, bringing us down to 230:

String t(String s, Integer l) {
    if(s.size < l) {
        return s;
    }
    value i = s[0:l-2].lastIndexWhere((e) => e == ' ' || e == '-');
    if(exists i) {
        return s[0:i] + "...";
    }
    return s[0:l-3]+"...";
}

Instead of e == ' ' || e == '-' we can also write e in [' ', '-'] (or e in {' ', '-'}, this is an iterable constructor instead of a tuple one). The in operator maps to the method Category.contains, which brings us to the idea that we can pass that tuple's contains method directly (it is a callable taking any object, so also accepting character), without the (e) => ... boilerplate (222 bytes):

String t(String s, Integer l) {
    if(s.size < l) {
        return s;
    }
    value i = s[0:l-2].lastIndexWhere([' ', '-'].contains);
    if(exists i) {
        return s[0:i] + "...";
    }
    return s[0:l-3]+"...";
}

Actually, another category containing the same two characters is the two-character string " -". (In addition it also contains its substrings, but that doesn't hurt here). 216 bytes.

String t(String s, Integer l) {
    if(s.size < l) {
        return s;
    }
    value i = s[0:l-2].lastIndexWhere(" -".contains);
    if(exists i) {
        return s[0:i] + "...";
    }
    return s[0:l-3]+"...";
}

I guess we got the most out of this line, let's turn to the others ... the last two return statements have some similarity which we can exploit – they just differ in i vs. l-3, and are using i just when it is not null, otherwise l-3. Fortunately this is exactly what the else operator is made for!

String t(String s, Integer l) {
    if(s.size < l) {
        return s;
    }
    value i = s[0:l-2].lastIndexWhere(" -".contains);
    return s[0:(i else l-3)] + "...";
}

(The parentheses seem to be needed here, as else has a lower precedence than [:].) This is 171 characters. Now i is used just once, so we can inline it, bringing us to 153 characters:

String t(String s, Integer l) {
    if(s.size < l) {
        return s;
    }
    return s[0:(s[0:l-2].lastIndexWhere(" -".contains) else l-3)] + "...";
}

We can also replace this if-return-return combination by a combination of the then and else operators in one return. (then returns is second operand when the first one is true, otherwise null, which then allows else to return its second operand.`) 131 bytes (although some of the savings are the white spaces which we'll get rid off anyways):

String t(String s, Integer l) {
    return s.size < l then s else s[0:(s[0:l-2].lastIndexWhere(" -".contains) else l-3)] + "...";
}

A function which contains just one return with an expression can alternatively be written with the "fat arrow" notation, giving 123:

String t(String s, Integer l) =>
    s.size < l then s else s[0:(s[0:l-2].lastIndexWhere(" -".contains) else l-3)] + "...";

Removing the unneeded whitespace gives us the final 111 bytes:

String t(String s,Integer l)=>s.size<l then s else s[0:(s[0:l-2].lastIndexWhere(" -".contains)else l-3)]+"...";

As an addition, here is a function which prints the examples from the question (using the name t which is used after step two):

shared void testTruncate() {
    value testInputs = {
        ["This is some very long text.", 25],
        ["This-is-some-long-hyphen-separated-text.", 33],
        ["Programming Puzzles & Code Golf is a question and answer site for programming puzzle enthusiasts and code golfers.", 55], 
        ["abcdefghijklmnopqrstuvwxyz", 20],
        ["a b c", 4],
        ["Very long.", 100]
    };
    for(input in testInputs) {
        print(t(*input));
    }
}
\$\endgroup\$
1
\$\begingroup\$

POSIX shell + GNU sed, 65 bytes

sed -re "s/(.{$1}).+/\1/;T;s/(.*)[- ]...*/\1.../;t;s/...$/.../;:"

This is a job made for sed! But I needed shell to get the length limit in (perhaps Perl would be better). The sed part expands out to a fairly simple sequence, with conditional jumps when we finish:

s/(.{$1}).+/\1/
T
s/(.*)[- ]...*/\1.../
t
s/...$/.../
:
\$\endgroup\$
1
\$\begingroup\$

Mathematica 192 bytes

t=With[{r=StringTake[#,Min[#2-2,StringLength[#]]],p={"-",Whitespace},e="..."}, 
  Which[StringLength[#]<=#2,#,StringFreeQ[r,p],StringDrop[r,-1]<>e,
   True,StringTake[r,Max[StringPosition[r,p]]-1]<>e]]&

Called as

t["This is some very long text.", 25]
\$\endgroup\$
1
\$\begingroup\$

><>, 74 bytes

l$-:1)?\~r05.
/?=0:~$<-1
\}:0=  ?\::"- "@=@=+?
>~"..."r\}
/!?     <
>ol?!;

This solution requires the string to be truncated and L to already be on the stack, in that order.

There's 7 wasted bytes caused by alignment issues, still trying to golf those out.

\$\endgroup\$
1
\$\begingroup\$

C# (157):

Based on the answer of Salah Alami, but shorter. The string class derives from IEnumerable<char>, so instead of T.Contains(" ")||T.Contains("-"), I use " -".Any(x=>T.Contains(x)).

Solution:

string f(string T,int L){if(T.Length<=L)return T;T=T.Substring(0,L-2);return T.Substring(0," -".Any(T.Contains)?T.LastIndexOfAny(new[]{' ','-'}):L-3)+"...";}

Ungolfed:

string f (string T, int L)
{
    if (T.Length <= L)
        return T;

    T = T.Substring(0, L - 2);

    return T.Substring(0, " -".Any(T.Contains) ? T.LastIndexOfAny(new[]{' ', '-'}) : L - 3) + "...";
}

Update:

Saved 6 bytes thanks to the comment of SLuck49, using Any(T.Contains) instead of Any(x=>T.Contains(x)).

\$\endgroup\$
  • 1
    \$\begingroup\$ For .Any(x=>T.Contains(x)) you can directly use the method instead of a lambda like .Any(T.Contains) to save 6 bytes \$\endgroup\$ – SLuck49 Oct 1 '15 at 19:29
  • \$\begingroup\$ @SLuck49 thanks, updated my answer. \$\endgroup\$ – Abbas Oct 2 '15 at 7:27
1
\$\begingroup\$

GS2, 29 bytes

This program takes standard input. The first line is the string, and the second is the target length number.

2a 0e 56 3c 40 a0 74 20 22 22 04 5d 2e 2a 3f 5b
20 2d 5d 7c 2e 07 2e 2e 2e 9d 20 e4 35

GS2 code can be a bit hard to read sometimes. :) Here's some commentary.

2a         # lines - split input on newlines yielding a two element array
0e         # extract-array - pop array, push both elements 
56         # read-num - convert length string to number
3c         # take - truncate the string to specified length
40         # dup - duplicate truncated string on stack
a0         # junk1 - push the last popped value, the un-truncated string
74         # ne - test for inequality
    20     # reverse string
    22 22  # tail tail - remove first two characters

    # regex replace first occurrence of ".*?[ -]|." with "..."
    04 5d 2e 2a 3f 5b 20 2d 5d 7c 2e 07 2e 2e 2e 9d 

    20     # reverse string
e4         # block5 - make a block out of last 5 instructions
35         # when - conditionally execute block
\$\endgroup\$
1
\$\begingroup\$

Groovy, 56 bytes

Copied Kleyguerth's answer first, hence the same variable names...

Trim the string down by 2 chars, then most of the work is done by the regex, replace a dash or a space followed by any number of chars that are not a dash or a space at the end of the string with a "." OR replace any character at the end of the string if all the chars before it are neither a dash or a space with a ".". Harder to put into words than to write the regex...

a={T,L->T.size()<=L?T:T[0..L-3].replaceAll("([- ][^ -]*|(?<=[^- ]*).)\$",".")+".."}

Edit: Actually, can just remove the part of the string that matches the regex and add "..." at the end:

a={T,L->T.size()<=L?T:T[0..L-3]-~/[- ][^ -]*$|.$/+"..."}
\$\endgroup\$
1
\$\begingroup\$

Jelly, 29 bytes

ḣṖṖe€⁾ -œṗƊṖȯ$Ẏḣ⁹_3¤”.⁺⁺ðL>¥¡

Try it online!

Full program.

\$\endgroup\$
1
\$\begingroup\$

Clean, 89 bytes

import StdEnv
$n l|size l>n=l%(0,last[n:[i\\i<-[2..n]&c<-:l|c==' '||c=='-']]-3)+++"..."=l

Try it online!

As a function $ :: Int String -> String

\$\endgroup\$
0
\$\begingroup\$

C# (Visual C# Interactive Compiler), 117 bytes

a=>b=>a.Length>b?a.Substring(0,(" -".Any(x=>a.IndexOf(x,0,b-2)>-1)?a.LastIndexOfAny(new[]{' ','-'},b-2):b-3))+"...":a

Based off of @Abba's, which is based off of @Salah Alami's answer. Instead of using Contains and an unnecessary Substring call, it uses IndexOf to check if a hyphen or space exists in the truncated string.

Try it online!

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.