ASCII art is fun. Modern text editors are very good at manipulating text. Are modern programming languages up to the task?

One common task in ASCII art manipulation is to crop text to a rectangle between two characters. This is the task you must implement in this challenge.

Details

Your program will take 3 inputs:

  • the first is the 'start' character of the block - marking the top-left corner
  • the second is the 'end' character of the block - marking the bottom-right corner
  • the third is some form of multiline text, either a string, or list of strings, or filename, or whatever

The result will be multiline text (again, in any of the above formats) cropped to the rectangle between the given inputs. Note that the first two inputs may not be unique.

Edge cases

Boxes must always have volume at least 2. Thus these:

()     (
       )

are boxes but these:

)(     )      (
       (     )

are not (with start=( and end=)).

The input will only contain one box. Thus the start and end characters must only occur once, unless they are the same character in which case they must occur exactly twice.

Additionally each line in the input must be at least as long as the distance from the start of a line to the right edge of the box in the input.

Your program does not need to handle invalid inputs; they may result in undefined behavior.

Rules

Typical code-golf rules apply. Shortest code wins.

Examples

Sunny-day: start: ( end: ) input:

This is some text
. (but this text
  is in a box  ).
So only it is important.

Output:

(but this text
is in a box  )

Note the stripping of horizontal space as well. ASCII art crops are 2d.

Rainy-day: start: ( end: ) input:

This is some text (
But is that even  )
really a box?

Output:

(
)

Same start/end: start: / end: / input:

Oh, I get how this could be useful
 /----------------------------\
 | All this text is in a box! |
 \----------------------------/

Output:

/----------------------------\
| All this text is in a box! |
\----------------------------/

Invalid input: start: ( end: ) input:

Boxes are rectangular ( so this has
0 volume ) which is illegal.

Invalid input 2: start: ( end: ) input:

(The lines must already be square 
so this line that is too short
relative to this end, is illegal)
  • What about a valid box with a line outside that is shorter than the box? – seadoggie01 Sep 13 at 17:18
  • 1
    clarified, also invalid input – LambdaBeta Sep 13 at 17:23
  • what should be the outcome in case of an invalid input? or are they mentioned so they won't have to be taken care of? – Uriel Sep 13 at 17:26
  • 1
    The outcome is much like undefined behaviour in C, don't worry about it, anything goes. – LambdaBeta Sep 13 at 17:32
  • This is a nasty little challenge: nice job! – seadoggie01 Sep 13 at 17:42

11 Answers 11

Vim, 16, 12 bytes/keystrokes

#<C-v>Nj*yggVGp

Try it online! in the V interpreter

Modern text editors are very good at manipulating text. Are modern programming languages up to the task?

I bet old text editors are even better! :D

Even though it doesn't necessarily have to, this answer does work with both of the given "invalid" inputs, outputting

 rectangular (
) which is ill

and

(The lines must already be square
so this line that is too short
relative to this end, is illegal)

Explanation:

#               " Move backward to the previous occurrence of the word (or in this case, character) under the cursor
 <C-v>          " Start a visual block selection
      N         " Go to the next occurrence of the last searched term (guaranteed to be line 1)
       j        " Move down a line
        *       " Move forward to the next occurrence of the character under the cursor
         y      " Yank (copy) the whole visually selected block
          gg    " Go to line 1
            VG  " Select every line
              p " And paste what we last copied over it, deleting the whole buffer and replacing it with the block
  • 1
    Incidentally this is exactly the use-case I was doing to prompt me to write this challenge. I had my q macro as /\/<cr><c-v>nygv$o0dp or something like that for far too long :) – LambdaBeta Sep 13 at 17:34
  • 2
    Yeah, rectangular is the illest! – AdmBorkBork Sep 13 at 20:03

Jelly, 13 bytes

=€SŒṪr/,þ/Zœị

A dyadic link accepting a list of start and end characters on the left and a list of lines (as lists of characters) on the right which yields a list of lines (as lists of characters).

Try it online! (full program - if inputs are valid Python they will require Python-string-quoting.)

How?

=€SŒṪr/,þ/Zœị - Link: [start, stop], lines
 €            - for each (of [start, stop]):
=             -   equals? (vectorises across the lines)
  S           - sum (vectorises)
   ŒṪ         - multi-dimensional truthy (i.e. non-zero) indices
      /       - reduce by:
     r        -   inclusive range (vectorises)
         /    - reduce by:
        þ     -    outer product with:
       ,      -       pair
          Z   - transpose
           œị - multi-dimensional index-into (the lines)

As an example, with left = ['a', 'b'] and right (as a list of lists of characters - the lines):

--------
--a+++--
--++++--
--+++b--
--------

=€ yields a list of two lists of lists (the first performs 'a'=, the second 'b'=):

00000000         00000000
00100000         00000000
00000000    ,    00000000
00000000         00000100
00000000         00000000

summing this yields a single list of lists (summing element-wise):

00000000
00100000
00000000
00000100
00000000

ŒṪ then gives us the (1-indexed) multi-dimensional indices of the non-zeros, [[2,3],[4,6]] - i.e. [[top,left],[bottom,right]].

r/ then performs [2,3]r[4,6] which, since r vectorises, is like [2r4, 3r6], evaluating to [[2,3,4],[3,4,5,6]] - i.e. [rows,columns].

,þ/ then performs [2,3,4],þ[3,4,5,6] where þ is an outer-poduct instruction and , is pair. This yields all the [row,column] values by column, in this case:

[[[2,3],[3,3],[4,3]],
 [[2,4],[3,4],[4,4]],
 [[2,5],[3,5],[4,5]],
 [[2,6],[3,6],[4,6]]]

We want these by row, so Z is used to transpose this to:

[[[2,3],[2,4],[2,5],[2,6]],
 [[3,3],[3,4],[3,5],[3,6]],
 [[4,3],[4,4],[4,5],[4,6]]]

Finally œị indexes back into the input lines:

a+++
++++
+++b

It's worth noting that when both bounding characters are the same =€ identifies both twice but SŒṪ ends up doing the right thing, since 2 is truthy, e.g. with ['a','a']:

--------         00000000   00000000        00000000
--a+++--         00100000   00100000        00200000
--++++--  =€ ->  00000000 , 00000000  S ->  00000000  ŒṪ ->  [[2,3],[4,6]]
--+++a--         00000100   00000100        00000020
--------         00000000   00000000        00000000
  • ... I read the explanation, but I still don't understand it. o_o Could you add a worked example, perhaps? – DLosc Sep 19 at 19:53
  • Incentivisation: I'll accept your answer if it is fully explained. :) – LambdaBeta Sep 19 at 19:57
  • 1
    @DLosc - done, hope it helps. – Jonathan Allan Sep 19 at 21:40
  • @LambdaBeta - the V answer is shorter. – Jonathan Allan Sep 19 at 21:41
  • ...err the Vim answer even. – Jonathan Allan Sep 22 at 21:19

APL (Dyalog), 38 30 bytes

4 bytes saved thanks to @EriktheOutgolfer

(1-⍨w-⍨⊃⍸⎕=s)↑(w←∊⊃⌽⍸⎕=s)↑s←↑⎕

Try it online!

  • too complicated. you could accept a matrix instead of vector of vectors, find the two positions with ⍸matrix∊separators, and do take/drop with them – ngn Sep 14 at 11:31
  • (⍸a=⎕)↓(1+⍸a=⎕)↑a←⎕ with ⎕io←0 – ngn Sep 14 at 11:45
  • @ngn OP said number of characters differs between lines so I assumed input should be vector before processing. Even so, I need the selection parts (first and rotate) in case the delimiter shows a few times (see third test case), but I guess the drop does cut a few bytes, so thanks! I'll update once I get to PC – Uriel Sep 14 at 15:21
  • oops... I forgot about the third case, sorry. Then maybe: ⊃{⌽⊖⍵↓⍨⊃⍸⍺=⍵}/⎕⎕⎕ (sic, with 3 trailing quads) which is even shorter. Or ... ⎕⎕(↑⎕) if a pre-mixed matrix is not allowed. – ngn Sep 14 at 16:12

Jelly, 14 bytes

œẹⱮẎQr/Ṛṭþ/œị⁸

Try it online!

  • I tried running your code on some of the other examples, but only got a crash. Is that a bug, or am I just doing something wrong? – Ilmari Karonen Sep 13 at 19:08
  • @IlmariKaronen You haven't quoted the second argument properly (didn't mention that in the post); wrap it in single or double quotes. The way you've invoked it, the second argument is an empty (Python) tuple (()), not '()'. If it's parse-able, it needs to be quoted, however, my // doesn't need to be quoted (integer division operator without operands? hm...). – Erik the Outgolfer Sep 13 at 19:10
  • @IlmariKaronen I "think" that () is just being interpreted by Jelly as some kind of special character. Most pairs of characters I try work. I'd love to hear what people more familiar with Jelly think. EDIT: ninja-ed by erik the outgolfer – LambdaBeta Sep 13 at 19:11
  • OK, yeah, that works. – Ilmari Karonen Sep 13 at 19:17

Python 2, 130 bytes

def f(s,e,t):
 a=[(i,l.find(c))for i,l in enumerate(t)for c in s+e if c in l];r,c,R,C=a[0]+a[-1]
 for l in t[r:R+1]:print l[c:C+1]

Try it online!

Canvas, 37 bytes

{³⁴⁰;x≡‽┐
X⁸)J╵⁶;┤ω┤⁵X⁶⁸⁰K├;┐┤└∔┘┘∔;@

Try it here!

36 bytes for getting the coordinates of the characters (and converting them to x,y,w,h because that's what takes) and 1 byte for getting the subsection.. There must be a better approach

JavaScript (ES6), 98 bytes

Takes input as two integers and an array of strings. Returns an array of strings.

(x,y,a,X=Y=0)=>a.filter(s=>!Y&&(Y=-~s.indexOf(y,X?X-1:X=-~s.indexOf(x)),X)).map(s=>s.slice(X-1,Y))

Try it online!

Commented

( x,                          // x = start character
  y,                          // y = end character
  a,                          // a[] = array of strings
  X =                         // X = position of x, plus 1
  Y = 0                       // Y = position of y, plus 1
) =>                          //
  a.filter(s =>               // for each string s in a[]:
    !Y &&                     //   reject this string if Y is non-zero
    (                         //   otherwise, use the 2nd condition:
      Y = -~s.indexOf(        //     update Y:
        y,                    //       by looking for y in s
        X ?                   //       if X is non-zero:
          X - 1               //         start the search at X - 1
        :                     //       else:
          X = -~s.indexOf(x)  //         update X and start the search at X
      ),                      //     end of Y update
      X                       //     keep this string if X is non-zero
    )                         //   end of 2nd condition
  )                           // end of filter()
  .map(s =>                   // for each remaining string s:
    s.slice(X - 1, Y)         //   remove left and right characters outside the box
  )                           // end of map()
  • filter and map?! Would building a new array with reduce or a recursive solution not work out shorter? On me phone, down the pub or I'd give it a go myself. – Shaggy Sep 13 at 22:25
  • @Shaggy There's probably a shorter way indeed, but I think that this method is doomed to use 2 passes: the 2nd loop cannot start before the 1st one terminates and both X and Y are known for sure. – Arnauld Sep 13 at 23:00

Java 10, 204 bytes

(s,e,a)->{int b=-1,i=0;for(;i<a.length;i++)a[i]=(b=b<0?a[i].indexOf(s):b)<0|a[i].length()<b?"":a[i].substring(b);for(b=-1;i-->0;)a[i]=(b=b<0?a[i].indexOf(e):b)<0|a[i].length()<b?"":a[i].substring(0,b+1);}

Modifies the input-array instead of returning a new one to save bytes. This does mean that removed lines become "" instead, though. If this is not allowed I will change it.

Try it online.

Explanation:

(s,e,a)->{                 // Method with 2 Strings & String-array parameters and no return
  int b=-1,                //  Boundaries-integer, starting at -1
  i=0;for(;i<a.length;i++) //  Loop `i` in the range [0, amountOfLines)
    a[i]=                  //   Change the `i`th line in the array to:
      (b=b<0?              //    If `b` is -1:
          a[i].indexOf(s)  //     Set `b` to the index of `s` in the current line
                           //     (which is still -1 if it's not found)
         :                 //    Else (starting index already found)
          b                //     Leave `b` unchanged
      )<0                  //    Then, if `b` is -1,
         |a[i].length()<b? //    or the current line-length is too short:
       ""                  //     Remove the current line
      :                    //    Else:
       a[i].substring(b);  //     Shorten the line by removing every character before `b`
  for(b=-1;                //  Reset `b` to -1
      i-->0;)              //  Loop `i` in the range (amountOfLines, 0]
    a[i]=                  //  Change the `i`th line in the array to:
       (b=b<0?a[i].indexOf(e):b)<0|a[i].length()<b?"":
                           //   Similar as above (with end `e` instead of start `s`),
         a[i].substring(0,b+1);}
                           //   except we remove every character after `b` this time

For example:

With the inputs start = "(", end = ")" and lines =

["This is some text",
 ". (but this text",
 "  is in a box  ).",
 "So only it is important."]

the first loop will crop it at the top and left, changing it to this:

["",
 "(but this text",
 "is in a box  ).",
 " only it is important."]

the second loop will crop it at the bottom and right, changing it to this:

["",
 "(but this text",
 "is in a box  )",
 ""]

Retina 0.8.2, 110 bytes

^((.)¶.)(.*¶)+(.*\2)
$1¶$4
^(.)(¶.¶\1)
$2
}s`(?<=^.¶.+)¶.
¶
s`^¶(.)¶(.*\1).*
$2
+m`^((.)+).¶((?<-2>.)+)$
$1¶$3

Try it online! Explanation:

^((.)¶.)(.*¶)+(.*\2)
$1¶$4

Delete the input lines preceding the first line of the box.

^(.)(¶.¶\1)
$2

If the start character is in the left column of the input, delete it.

}s`(?<=^.¶.+)¶.
¶

If the start character hasn't been deleted yet, shift all the input columns left by one and repeat from the beginning.

s`^¶(.)¶(.*\1).*
$2

Delete the end character and everything in the input after the end character.

+m`^((.)+).¶((?<-2>.)+)$
$1¶$3

Truncate all the lines to the length of the next line. This works by counting all but one character on each line, then attempting to match up to that many characters on the next line; if this succeeds then the second line was shorter, so the last character is deleted and the loop repeats.

C (gcc), 237 bytes

f(c,r,o,p)char*p,*c;{char*_=strchr(p,r),*a,b;*_=0;a=strrchr(p,10);a=(a?a:p);*_=r;r=_-a;p=a;_=strrchr(p,o);*_=0;a=strrchr(p,10);a=(a?a:p);*_=o;o=_-a+1;_[1]=0;for(_=p;_;_=strchr(_+1,10)){b=_[o];_[o]=0;strcat(c,_+r);strcat(c,"\n");_[o]=b;}}

Try it online!

I'm 99% sure this can be shortened using some kind of helper function to find horizontal index of and pointer to a character, as it is repeated twice. Alas I couldn't find a short enough way to do it, I may try again later if I find the time.

Description

f(c,r,o,p)char*p,*c;{
    char*_=strchr(p,r),*a,b;         // find opening char (and declare vars)
    *_=0;a=strrchr(p,10);            // find \n before it
    a=(a?a:p);                       // deal with single line inputs
    *_=r;r=_-a;                      // save left margin width in r
    p=a;                             // crop everything before opening line

    _=strchr(p,o);                   // find closing char
    *_=0;a=strrchr(p,10);            // find \n before it
    a=(a?a:p);                       // deal with single line inputs
    *_=o;o=_-a+1;                    // save width in o
    _[1]=0;                          // crop everything after closing char
    for(_=p;_;_=strchr(_+1,10)){       // for each line
        b=_[o];_[o]=0;
        strcat(c,_+r);
        strcat(c,"\n");
        _[o]=b;
    }
}

Stax, 15 bytes

╛↨½╝v∞░W╧)╗Ö≈☼k

Run and debug it

It takes the set of box delimiter characters (1 or 2) on the first line of input. The rest of the lines are the input body.

Unpacked, ungolfed, and commented, it looks like this.

            first line of input is the delimiter characters
dL          discard the first line of input and listify the rest into an array
{           begin block for iteration
  Mr        rotate matrix 90 degrees
  {         begin block for while loop
    ch      copy first row of block
    y|&C    if it insersects with the first line of input, break iteration
    D       drop the first line
  W         do-while loop until break
}4*         execute block 4 times
m           display result lines

Run this one

Your Answer

 

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

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