16
\$\begingroup\$

The Challenge

Given a multiline string, return the longest substring of leading whitespace present in every line, ignoring empty lines or lines composed only of whitespace. If input is empty or whitespace, return nullish or the empty string--up to you.

Update: Per Meta consensus, the input may also be treated as an array of strings. My examples below could be shortened by excluding the .split() calls.


I wrote a quick script for my iPhone that comments or uncomments JavaScript code (since I can't use ctrl+/).

I want the // placed the furthest right that it can, given that each // is aligned with the other lines. If a line is only whitespace (or empty), I want to skip adding a comment.

To do this, I must determine the longest leading whitespace substring that exists in every line. So, given the input string

  const x = () =>
    y.split()
      .map(s =>
        s.length
      )
      .join()
    
  console.log(x)

the result of the algorithm would be 2 spaces. (Note: Line 7 has 4 leading spaces, but the browser may or may not remove them. It does not appear to remove them in the example solutions below.) If the input was only lines 3-7, the result would be 6 spaces.

A simple JS solution to achieve this would be

const solution = text => {
  const lines = text.split('\n').filter(l => !!l.trim())

  let result = ''
  while (lines.every(l => l.startsWith(result + ' '))) {
    result += ' '
  }

  return result
}

// test

const text = 
`  const x = () =>
    y.split()
      .map(s =>
        s.length
      )
      .join()

  console.log(x)`

let result = solution(text);
console.log(`|${result}| ${result.length} spaces`)

const lines3Through7 = text.split('\n').slice(2, 7).join('\n')

result = solution(lines3Through7)
console.log(`|${result}| ${result.length} spaces`)

result = solution(
`abc
  def`)
console.log(`|${result}| ${result.length} spaces`)

I'm sure you can do better than this.

Another would be

const solution = text =>
  ' '.repeat(
    text.split('\n')
      .filter(l => !!l.trim())
      .map(l => l.length - l.trimStart().length)
      .reduce((m, v) => Math.min(m, v))
  )

// test

const text =
`  const x = () =>
    y.split()
      .map(s =>
        s.length
      )
      .join()
    
  console.log(x)`

let result = solution(text);
console.log(`|${result}| ${result.length} spaces`)

const lines3Through7 = text.split('\n').slice(2, 7).join('\n')

result = solution(lines3Through7)
console.log(`|${result}| ${result.length} spaces`)

result = solution(
`abc
  def`)
console.log(`|${result}| ${result.length} spaces`)


Assume all whitespaces are space characters (i.e. not \t). For input that has no non-whitespace characters, it should return nullish or 0 spaces, up to you.

You can use any language, but I'd also like someone to present an answer in JavaScript ✅.

Shortest answer wins. Include your answer, a prettified version of your answer for readability, and for esoteric languages, an explanation of how your script works.

\$\endgroup\$
5
  • 1
    \$\begingroup\$ Is the input guaranteed to contain at least one line which is neither empty nor composed only of whitespace? \$\endgroup\$
    – Arnauld
    Feb 14, 2022 at 22:40
  • 1
    \$\begingroup\$ Yes, that was supposed to read lines 3-7, the result would be 6 spaces. And no, not guaranteed. \$\endgroup\$
    – dx_over_dt
    Feb 14, 2022 at 23:02
  • 4
    \$\begingroup\$ Can we assume the input only contains printable ASCII including spaces (and newlines)? \$\endgroup\$
    – pxeger
    Feb 15, 2022 at 12:44
  • 2
    \$\begingroup\$ What should be the output for an input that consists of nothing but empty/whitespace-only lines? \$\endgroup\$
    – DLosc
    Feb 15, 2022 at 16:30
  • \$\begingroup\$ @DLosc updated! Return nullish or the empty string, up to you. \$\endgroup\$
    – dx_over_dt
    Feb 15, 2022 at 20:45

19 Answers 19

7
\$\begingroup\$

Jelly, 9 8 bytes

=⁶Pi0’⁶ẋ

Try it online!

-1 thanks to Jonathan Allan reminding me of meta consensus on being able to take a list of lines.

=⁶          For each character on each line, is it a space?
  P         Take the product along each column,
   i0       and find the first 1-index of 0 (or 0 if not found).
     ’      Decrement
      ⁶ẋ    and yield a space repeated that many times (or none if not positive).

n⁶SĬS⁶ẋ works similarly, but does not ignore all-whitespace lines if there's nothing else.

If we can assume the input contains no codepoints under 32 other than newlines, this almost works:

Jelly, 7 6 5 bytes

Ṁ»\f⁶

Try it online!

-1 because I tried replacing »/ with just in case that would work, and after further consideration, of course it would work--the lexicographically greatest line is precisely some line with the fewest leading spaces that has something else after them (under the assumption stated above).

-1 thanks to Jonathan Allan reminding me of meta consensus on being able to take a list of lines.

Fails to ignore all-whitespace lines if there's nothing else. A fix in fewer than 3 bytes seems unlikely (though Ṁ»\ḟȧfɗ⁶ in 3 bytes seems worth mentioning).

Ỵ         Split on newlines.
 Ṁ        Take the lexicographically greatest row,
  »\      find the largest character in each of its prefixes,
    f⁶    and remove any that aren't spaces.
\$\endgroup\$
3
  • 3
    \$\begingroup\$ These do not deal with the edge case where all the lines in the input are space-only. As I read it such an input should yield an empty string. \$\endgroup\$ Feb 15, 2022 at 12:27
  • 1
    \$\begingroup\$ Also, we can actually take a list of lines by Meta consensus. \$\endgroup\$ Feb 15, 2022 at 13:01
  • 1
    \$\begingroup\$ What a horrible, horrible language. 😆 Well done! \$\endgroup\$
    – dx_over_dt
    Feb 15, 2022 at 20:48
5
\$\begingroup\$

Jelly,  10  9 bytes

-1 by using the default ruling that Submissions may use list of strings instead of multi-line strings.

n⁶T€FṂ’⁶ẋ

A monadic Link accepting a list of lines (lists of characters) that yields a list of characters.

Try it online!

How?

n⁶T€FṂ’⁶ẋ - Link: list lists of characters, Lines
 ⁶        - space character
n         - not equals? (vectorises across all the characters in Lines)
   €      - for each:
  T       -   truthy indices (1-indexed)
    F     - flatten
     Ṃ    - minimum (minimum of the empty list is 0)
      ’   - decrement -> number of "common" leading spaces, N
       ⁶  - space character
        ẋ - repeat N times (repeating -1 or 0 times gives an empty list)
\$\endgroup\$
4
\$\begingroup\$

05AB1E (legacy), 9 bytes

εDðKk}ßð×

Input as a list of character-lists.

Try it online (footer is added to pretty-print the string of spaces) or verify all test cases.

Explanation:

ε      # Map over each inner character-list of the (implicit) input-list:
 D     #  Duplicate the current character-list
  ðK   #  Remove all spaces from the copy
    k  #  Get the first 0-based index of each of these characters in the list
}ß     # After the map: pop and push the flattened minimum
  ð×   # Convert it to a string with that many spaces
       # (after which the result is output implicitly)
\$\endgroup\$
0
3
\$\begingroup\$

Retina 0.8.2, 16 bytes

G`\S
O^`.+
!`^ +

Try it online! Explanation:

G`\S

Discard lines that only consist of whitespace.

O^`.+

Sort them in reverse order, so the line with the least space sorts first.

!`^ +

Output the leading spaces.

If just the count of leading spaces suffices, then for 14 bytes:

G`\S
O^`.+
\G 

Try it online! Explanation: The last stage \G counts consecutive spaces at the start of the buffer.

\$\endgroup\$
3
\$\begingroup\$

Vyxal, 10 bytes

↵ƛðv≠TgI;g

Try it Online!

A port of the jelly answer. 8 bytes with the g flag.

Explained

↵ƛðv≠TgI;g
↵ƛ          # For each item n in the input split on newlines:
  ðv≠       #   Is each character not a space?
     Tg     #   Smallest index where ^ is truthy. Returns an empty list if all spaces. 
       I    #   Push that many spaces or two empty lists if it's all spaces.
        ;g  #   Return the smallest item of that. 
\$\endgroup\$
1
  • 2
    \$\begingroup\$ There no longer is "the" Jelly answer ;-) \$\endgroup\$
    – pajonk
    Feb 15, 2022 at 8:06
3
\$\begingroup\$

Python, 39 bytes

lambda l:max(l)[:-len(max(l).lstrip())]

Attempt This Online!

Takes input as a list of lines, per default I/O rules.

Uses Unrelated String's observation that the lexicographically greatest line will be the one with the least leading whitespace. This means the input must not contain unprintables other than newline.

\$\endgroup\$
1
  • 1
    \$\begingroup\$ I don't think we can assume that characters less than spaces will never appear. You should confirm that before using it in your answer. \$\endgroup\$ Feb 15, 2022 at 12:39
3
\$\begingroup\$

Husk, 9 11 10 9 bytes

Thanks to Jonathan Allan for bug-spotting (both versions)

←T↑o¬wTfw

Try it online!

Takes a list of lines, removes all-space lines (fw), transposes (T), keeps longest prefix of all spaces (↑o¬w), transposes back again (T), and output first element ().


Husk, 7 9 8 bytes

↑=' →Ofw

Try it online!

Uses Unrelated string's lexicographic sorting approach: Takes a list of lines, removes all-space lines (fw), sorts (O), keeps last element (), and keeps the longest prefix of spaces (↑=' ).

\$\endgroup\$
1
  • 2
    \$\begingroup\$ You're right. Drat. Should be fixed now. \$\endgroup\$ Feb 15, 2022 at 12:59
3
\$\begingroup\$

JavaScript (ES6), 43 bytes

s=>(s.match(/^ *(?=\S)/gm)||[""]).sort()[0]

Try it online!

Or 40 bytes by using optional chaining, as suggested by the OP:

s=>s.match(/^ *(?=\S)/gm)?.sort()[0]||''

(doesn't work on TIO)

\$\endgroup\$
9
  • \$\begingroup\$ ... why did I not think of this? \$\endgroup\$
    – dx_over_dt
    Feb 14, 2022 at 22:57
  • \$\begingroup\$ With the optional chaining syntax ?., you could save 3 bytes. s=>s.match(/^ *(?=\S)/gm)?.sort()[0]||'' \$\endgroup\$
    – dx_over_dt
    Feb 14, 2022 at 23:05
  • 5
    \$\begingroup\$ Thank you for accepting this answer, but I'd suggest to un-accept it. We usually do not accept any answer at all on Code Golf -- or there should be a delay of at least one week or so before doing so. \$\endgroup\$
    – Arnauld
    Feb 14, 2022 at 23:06
  • \$\begingroup\$ Why need you ||[""] if OP's first example don't handle empty? \$\endgroup\$
    – l4m2
    Feb 15, 2022 at 3:07
  • 1
    \$\begingroup\$ Oh, good point. FWIW, I was a bit confused because I only saw @l4m2's second reply (my eyes skipped over the reply above it), and I thought you got the idea for the (?!$) from the ||[""] suggestion. In any case, you could get -2 bytes by changing ||[""] to ||[] to return undefined (instead of empty string) for any input containing no lines that are not pure whitespace. That could be interpreted as returning "nullish or the empty string" as per OP's instructions. \$\endgroup\$
    – Deadcode
    Feb 16, 2022 at 0:47
3
\$\begingroup\$

R, 50 62 60 57 bytes

Edit: -2 bytes thanks to pajonk

sub("^( *).*","\\1",sort(sub(" *$","",readLines()),T)[1])

Try it online!

\$\endgroup\$
2
3
\$\begingroup\$

Haskell, 47 46 bytes

fst.minimum.filter((>[]).snd).map(span(==' '))

Try it online!

Nothing special here, it's a very standard approach, just with less whitespace. A more presentable version:

main :: IO ()
main = interact (f . lines)

f :: [String] -> String
f = fst                      -- extract it from the tuple
  . minimum                  -- find the shortest leading whitespace (tuples are sorted by first element)
  . filter ((> []) . snd)   -- remove any tuples for which 'everything else' is empty
  . map (span (== ' '))      -- split each line into tuples of (leading whitespace, everything else)

Since minimum is a partial function, it will raise an exception if given input that consists only of whitespace. From my perusal of the rules, I'm under the impression that this is acceptable; but I'm fairly new here so please correct me if I'm wrong.

\$\endgroup\$
8
  • 1
    \$\begingroup\$ ...actually, I have no idea where that << is coming from, so maybe this is preferable \$\endgroup\$ Feb 16, 2022 at 1:37
  • 1
    \$\begingroup\$ @UnrelatedString I can't get that to typecheck :( My experience with Haskell on TIO is that it's a little bit contrived; the actual part where I count the bytes is a comment. The real code being executed is in the header. (Open to other suggestions, though.) So your TIO links are basically running my code :) \$\endgroup\$ Feb 16, 2022 at 1:55
  • 1
    \$\begingroup\$ ...didn't quite notice that :| For future reference this is how Haskell answers are usually done \$\endgroup\$ Feb 16, 2022 at 1:57
  • 1
    \$\begingroup\$ @UnrelatedString Ah, ok. Thanks a lot! That's so much better. \$\endgroup\$ Feb 16, 2022 at 2:00
  • 2
    \$\begingroup\$ I’m on my phone so I can’t test, but could this be implemented as the following? minimum.filter(>[]).map(takeWhile(==’ ‘) (note the smart quotes - I can’t seem to figure out how to be rid of them). At the very least you should be able to use > in lieu of /=. Also with the use of both filter and map a list comprehension might be worth the additional overhead. \$\endgroup\$
    – cole
    Feb 17, 2022 at 10:17
2
\$\begingroup\$

Excel, 48 58 bytes

Revised to return N spaces instead of just the value of N.

=LET(t,TRIM(A:A),REPT(" ",MIN(IF(t="","",FIND(t,A:A)))-1))

Trim the leading and trailing white space from each line then find the position what's left in the original line, ignoring those lines that are empty once they're trimmed. Find the minimum of those positions minus 1. Return a space repeated that many times.

Screenshot

Screenshot shows formula returning repeated pipes instead of spaces so you can see the results.

\$\endgroup\$
2
  • 1
    \$\begingroup\$ @KevinCruijssen Thanks, it's been corrected now. \$\endgroup\$ Feb 15, 2022 at 20:36
  • 1
    \$\begingroup\$ Lol, I had not considered Excel as a language. Awesome! \$\endgroup\$
    – dx_over_dt
    Feb 16, 2022 at 16:53
2
\$\begingroup\$

Pip -r, 15 bytes

@SS Ks~Y#||_FIg

Takes input from stdin; prints a string of spaces (with a trailing newline) to stdout. Outputs nothing if the input contains only whitespace. Attempt This Online!

Explanation

The -r flag reads all of stdin and stores it as a list of lines in g. Then:

@SS Ks~Y#||_FIg
            FIg  ; Filter g on this function:
         ||      ;   Strip whitespace from
           _     ;   each line
        #        ;   and get the length of what remains
       Y         ; Yank the result (used here just to manipulate precedence)
      ~          ; Get first match of this regex in (each line of) the above:
     s           ;   Preset variable: space
    K            ;   Kleene star (results in this regex: ` *`)
 SS              ; Sort in lexicographic order (in this case, shortest first)
@                ; Get the first element
\$\endgroup\$
0
2
\$\begingroup\$

JavaScript (Node.js), 39 bytes

s=>/( *)\S(.|\s+(\1|$))*$|$/.exec(s)[1]

Try it online!

Return undefined if no line

JavaScript (Node.js), 37 bytes

s=>/( *)\S(.|\s+(\1|$))*$/.exec(s)[1]

Try it online!

Throw if no line

\$\endgroup\$
2
  • \$\begingroup\$ OP said to return nullish or the empty string and I would not interpret throwing to count as that... But you can return undefined for only +2 bytes. \$\endgroup\$
    – Deadcode
    Feb 16, 2022 at 0:56
  • 1
    \$\begingroup\$ Also I'd like to note that this is a pure-regex solution and thus could also be answered in the language "Regex (ECMAScript)". I was going to do so, but I can't think of any solution shorter than yours. \$\endgroup\$
    – Deadcode
    Feb 16, 2022 at 1:01
1
\$\begingroup\$

Charcoal, 23 bytes

WS⊞υι× ∨⌊ΦEυ⌕E⪪ι ¬λ⁰⊕ι⁰

Try it online! Link is to verbose version of code. Takes input by default as a list of newline-terminated strings, so if you want to provide an empty string as part of the input you'll have to encode it as a JSON list instead. Explanation:

WS⊞υι

Input the string.

× ∨⌊ΦEυ⌕E⪪ι ¬λ⁰⊕ι⁰

Split each line on spaces and find the index of the first non-space character, but filter out those lines that only have spaces, and then take the minimum number of spaces, or 0 if all lines only have spaces, and repeat a space that many times.

\$\endgroup\$
2
  • 1
    \$\begingroup\$ @JonathanAllan I hadn't accounted for the edge case when all the input lines only contain spaces, in which case I'm trying to repeat a string by the minimum of an empty list, which doesn't work. \$\endgroup\$
    – Neil
    Feb 15, 2022 at 13:34
  • 1
    \$\begingroup\$ @JonathanAllan Sometimes I wish the sum or minimum of an empty list would be zero like it is in Jelly... at least for sum I have a workaround by using base 1 conversion instead, which is only 1 byte longer. \$\endgroup\$
    – Neil
    Feb 15, 2022 at 13:36
1
\$\begingroup\$

Ruby, 29 bytes

->a{a.map{_1[/^ +/]}.sort[0]}

Attempt This Online!

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

Perl 5, 38 bytes

print[sort map/^ +/g,grep/\S/,<>]->[0]

Try it online!

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

Burlesque, 23 bytes

t]:nzm{Jt[.-}saq<]qesIE

Try it online!

t]:nz  # Filter null/wspc lines
m{     # Map
 J     # Dup
 t[    # Trim left
 .-    # String Difference
}      -- Diff pre/post trim
sa     # non-destructive Length
q<]    # If non-zero length find max
qes    # Else Empty Block -> Empty string
IE     # If Else
\$\endgroup\$
3
  • 1
    \$\begingroup\$ @JonathanAllan Darn, missed that element. This should work now. \$\endgroup\$ Feb 15, 2022 at 14:58
  • 2
    \$\begingroup\$ TIO version seems to output the longest of all whitespace prefixes, not the longest present in every line (which, confusingly, is the shortest...) try it \$\endgroup\$ Feb 15, 2022 at 16:02
  • \$\begingroup\$ In my haste, I'd completely misunderstood the question. \$\endgroup\$ Feb 15, 2022 at 16:51
0
\$\begingroup\$

Scala, 51 bytes

"\\n[\\s]*".r.findAllIn("\n"+y).map(_.length).min-1

Try it online!

Explanation:

"\\n[\\s]*".r                   // regex to find newlines and all whitespaces after
           .findAllIn(          // iterator of all matches in the given string
                      "\n"+y)   // add a newline to the front to count first line
           .map(_.length).min   // find the shortest match
           -1                   // subtract one since lengths include the newlines
\$\endgroup\$
0
\$\begingroup\$

J-uby, 28 bytes

:*&(~:[]&/^ */)|:sort|:first

Attempt This Online!

Explanation

:* & (           # Map lines with...
   ~:[] & /^ */  #   Get part that matches /^ */
) |              # then
:sort | :first   # sort, then get the first
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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