Detect ASCII-art windows made of M and S characters

28

2

A Window is an ASCII-art square with odd side length of at least 3, with a single character border around the edge as well as vertical and horizontal strokes in the middle:

#######
#  #  #
#  #  #
#######
#  #  #
#  #  #
#######

An MS Window is a window where the border is made only of the characters M and S. Your task is to write a program (or function) that takes a string and outputs a truthy value if the input is a valid MS Window, and a falsey value if it is not.

Specifications

  • You may take the input as a newline-separated string or an array of strings representing each line.
  • The border of an MS Window may contain a mix of M and S characters, but the inside will always be composed of spaces.
  • You can choose to detect only windows with trailing newlines, or only windows without trailing newlines, but not both.

Test Cases

Truthy:

MMM
MMM
MMM

SMSMS
M M S
SMSMM
S S M
SMSMS

MMMMMMM
M  S  M
M  S  M
MSSSSSM
M  S  M
M  S  M
MMMMMMM

Falsey:

Hello, World!

MMMM
MSSM
MS M
MMMM

MMSMM
M S.M
sSSSS
M S M
MMSMM

MMMMMMM
M  M  M
MMMMMMM
M  M  M
MMMMMMM

MMMMMMM
M M M M
MMMMMMM
M M M M
MMMMMMM
M M M M
MMMMMMM

MMSSMSSMM
M   M   M
S   S   S
S   S  S
MMSSMSSMM
S   S   S
S   S   S
M   M   M
MMSSMSSMM

Esolanging Fruit

Posted 2017-01-08T05:49:45.603

Reputation: 13 542

3This is a great twist on ASCII arts, a decision problem to detect a certain structure. – xnor – 2017-01-08T05:53:08.343

4@xnor I feel like we might want a different tag for reverse ASCII art like this. – Esolanging Fruit – 2017-01-08T05:55:22.220

2while not specific to ascii art, pattern matching might be a good choice for a new tag – Destructible Lemon – 2017-01-08T06:15:27.150

Can you add a test case or two where the string doesn't form a rectangular array? – Greg Martin – 2017-01-08T08:54:59.243

Would MMSSM\nM S M\nMSSSS\nS M S\nMSSMS be valid? (the Ms and Ss are not constant) – user41805 – 2017-01-08T09:57:34.567

Related: Clean the muddy quartata-fish

– Blue – 2017-01-08T10:58:06.223

Can we assume the strings of the array will always be the same length (i.e. right-padded with spaces if needed)? – Luis Mendo – 2017-01-08T11:30:22.290

Why are the last two Falsey examples not Truthy? I'm probably missing something quite obvious here. – Mast – 2017-01-08T13:55:52.043

@Mast, I think because there should be four square panes – Chris M – 2017-01-08T16:26:45.577

@ChrisM That does not appear to be part of the requirement though. – Mast – 2017-01-08T17:14:12.477

1@Mast, you are quite right! Maybe the challenge needs clarifying – Chris M – 2017-01-08T17:19:44.123

I suppose M is a valid MS window too? – Adám – 2017-01-08T19:15:59.290

@Adám NO because A Window is an ASCII-art square with odd side length of at least 3, with a single character border around the edge as well as vertical and horizontal strokes in the middle (that's the first sentence of the challenge) yours has length 1 – edc65 – 2017-01-08T21:26:19.400

@Mast A Window is an ASCII-art square with odd side length of at least 3, with a single character border around the edge as well as vertical and horizontal strokes in the middle The last 2 falsey are not square (the first) and with more than 1 vertical and horizontal stroke in the middle (the last one) – edc65 – 2017-01-08T21:29:11.683

If we take the input as a single string, can we assume there's a trailing newline? – Patrick Roberts – 2017-01-08T21:46:46.593

@PatrickRoberts No, but you can decide whether a valid window has a trailing newline. – Esolanging Fruit – 2017-01-08T21:48:37.880

@Challenger5 hah, just now noticed your edit. Luckily my program still satisfies that requirement! – Patrick Roberts – 2017-01-08T23:00:37.277

Do we have to take into account different Windows / Linux line endings? – Mawg says reinstate Monica – 2017-01-09T08:54:49.383

@DestructibleWatermelon That tag used to exist but was removed some time ago, because it's usage was quite arbitrary. – Martin Ender – 2017-01-09T09:00:33.207

@edc65 I missed the last Falsey having an odd S, so I understand that one. The other one looks square though, 7x7. – Mast – 2017-01-09T17:29:39.943

Answers

1

Pyke, 34 31 bytes

lei}t\Mcn+it*i\M*+s.XM"QJ\S\M:q

Try it here!

lei                              -         i = len(input)//2
   }t                            -        (^ * 2) - 1
     \Mc                         -       "M".center(^)
        n+                       -      ^ + "\n"
          it*                    -     ^ * (i-1)
                 +               -    ^ + V
             i\M*                -     "M"*i
                  s              -   palindromise(^)
                   .XM"          -  surround(^, "M")
                               q - ^ == V
                       QJ        -   "\n".join(input)
                         \S\M:   -  ^.replace("S", "M")

Blue

Posted 2017-01-08T05:49:45.603

Reputation: 26 661

8

Retina, 68 67 bytes

Byte count assumes ISO 8859-1 encoding.

S
M
^(M((M)*M)\2)((?<-9>¶M((?<9-3> )*(?(3)!)M|\5)\5)*(?(9)!)¶\1)\4$

Try it online!

Martin Ender

Posted 2017-01-08T05:49:45.603

Reputation: 184 808

7

Grime, 39 38 bytes

Thanks to Zgarb for saving 1 byte.

e`BB/BB/W+ W/+
B=W|B/W\ * W/\ /*
W=[MS

Try it online!

I'm not sure whether there's a simpler way to enforce the square aspect ratio of the individual window components than using a recursive nonterminal, but this seems to be working quite well.

Explanation

It's best to read the program from the bottom up.

W=[MS

This simply defines a nonterminal (which you can think of as a subroutine that matches a rectangle) W which matches either an M or an S (there's an implicit ] at the end of the line).

B=W|B/W\ * W/\ /*

This defines a non-terminal B which matches about a quarter of the output, i.e. one window panel with the left and top border. Something like this:

MSM
S  
M  

To ensure that this window panel is square, we define B recursively. It's either a window character W, or it's B/W\ * W/\ /* which adds one layer to the right and to the bottom. To see how it does this, let's remove some syntactic sugar:

(B/W[ ]*)(W/[ ]/*)

This is the same, because horizontal concatenation can be written either AB or A B, but the latter has lower precedence than the vertical concatenation / while for the former has higher. So B/W[ ]* is a B with a window character and a row of spaces below. And then we horizontally append W/[ ]/* which is a window character with a column of spaces.

Finally, we assemble these nonterminals into the final window shape:

BB/BB/W+ W/+

That's four window panels B followed by a row of window characters and a column of window characters. Note that we make no explicit assertion that the four window panels are the same size, but if they aren't it's impossible to concatenate them into rectangle.

Finally the e` at the beginning is simply a configuration which tells Grime to check that the entire input can be matched by this pattern (and it prints 0 or 1 accordingly).

Martin Ender

Posted 2017-01-08T05:49:45.603

Reputation: 184 808

5

Perl, 124 123 119 95 93 84

The following Perl script reads one candidate MS Window from the standard input. It then exits with a zero exit status if the candidate is an MS Window and with a non-zero exit status if it isn't.

It works by generating two regular expressions, one for the top, middle and bottom line and one for every other line, and checking the input against them.

Thanks, @Dada. And again.

map{$s=$"x(($.-3)/2);$m="[MS]";($c++%($#a/2)?/^$m$s$m$s$m$/:/^${m}{$.}$/)||die}@a=<>

nwk

Posted 2017-01-08T05:49:45.603

Reputation: 621

I'm not sure giving the result as exit status is allowed (I don't have time to look for the relevant meta post though). Regardless, you can save a few bytes: @a=<>;$s=$"x(($.-3)/2);$m="[MS]";map{$a[$_]!~($_%($./2)?"$m$s$m$s$m":"$m${m}{$.}")&&die}0..--$. – Dada – 2017-01-08T12:44:26.060

@Dada: Thanks! That's an impressive improvement: 24 characters. (There was a stray "$m" in your code, so it's even shorter than it looked at first.) I wasn't sure if reporting the result with an exit code was allowed in general but I took the "write a program (or function)" as allowing one to be flexible with how the result is returned in this particular case; exit codes are practically the function return values of the *nix environment. :-) – nwk – 2017-01-08T22:38:36.340

Make that 26 characters. – nwk – 2017-01-08T22:44:49.147

1Actually, I'm decrementing $. at the end to avoid using twice $.-1 (especially since the first time it was ($.-1)/2 so it needed some extra parenthesis), so the $m in $m${m}{$.} isn't a mistake. Also, I just realized now, but the regexs should be surrounded with ^...$ (so extra character at the end or the beginning make them fail), or shorter: use ne instead of !~. – Dada – 2017-01-09T06:57:51.200

Nevermind, obviously you can't use ne instead of !~ (I shouldn't write messages when I've been awake for just 15 minutes!). So you'll have to use ^...$ in both regex I'm afraid. – Dada – 2017-01-09T08:05:11.140

I managed to golf it a little bit more (while adding the necessary ^...$: map{$s=$"x(($.-3)/2);$m="[MS]";($c++%($#a/2)?/^$m$s$m$s$m$/:/^${m}{$.}$/)||die}@a=<>. – Dada – 2017-01-09T09:07:06.537

Awesome! I really like this refactoring. I didn't realize Perl could do map{...}@a=<>. It's clever but makes you go "of course!" in retrospect. Thanks for correcting me on the $m/lack of ^...$. I forgot to account for removing the line length check early on that would have caught this. – nwk – 2017-01-11T07:33:10.323

5

JavaScript (ES6), 115 113 bytes

a=>(l=a.length)&a.every((b,i)=>b.length==l&b.every((c,j)=>(i&&l+~i-i&&l+~i&&j&&l+~j-j&&l+~j?/ /:/[MS]/).test(c)))

Takes input as a an array of arrays of characters (add 5 bytes for an array of strings) and returns 1 or 0. After verifying that the height is odd, every row is checked to ensure the array is square, and every character is verified to be one of the character(s) that we expect in that particular position. Edit: Saved 2 bytes thanks to @PatrickRoberts.

Neil

Posted 2017-01-08T05:49:45.603

Reputation: 95 035

You can change (...).includes(c) to ~(...).search(c) to save 1 byte – Patrick Roberts – 2017-01-08T21:15:04.840

1Actually, even better you can change it to (...?/ /:/[MS]/).test(c) to save 2 bytes instead of just 1. – Patrick Roberts – 2017-01-08T21:24:45.720

@PatrickRoberts Cute, thanks! – Neil – 2017-01-08T21:28:40.293

2

Mathematica, 166 bytes

Union[(l=Length)/@data]=={d=l@#}&&{"M","S"}~(s=SubsetQ)~(u=Union@*Flatten)@{#&@@(p={#,(t=#~TakeDrop~{1,-1,d/2-.5}&)/@#2}&@@t@#),p[[2,All,1]]}&&{" "}~s~u@p[[2,All,2]]&

Unnamed function taking a list of lists of characters as input and returning True or False. Here's a less golfy version:

(t = TakeDrop[#1, {1, -1, d/2 - 0.5}] &; 
Union[Length /@ data] == {d = Length[#1]}
  &&
(p = ({#1, t /@ #2} &) @@ t[#1];
SubsetQ[{"M", "S"}, Union[Flatten[{p[[1]], p[[2, All, 1]]}]]]
  && 
SubsetQ[{" "}, Union[Flatten[p[[2, All, 2]]]]])) &

The first line defines the function t, which separates a list of length d into two parts, the first of which is the first, middle, and last entries of the list, and the second of which is all the rest. The second line checks whether the input is a square array in the first place. The fourth line uses t twice, once on the input itself and once on all* of the strings in the input, to separate the characters that are supposed to be "M" or "S" from the characters that are supposed to be spaces; then the fifth and seventh lines check whether they really are what they're supposed to be.

Greg Martin

Posted 2017-01-08T05:49:45.603

Reputation: 13 940

2

JavaScript (ES6), 108 106 bytes

Input: array of strings / Output: 0 or 1

s=>s.reduce((p,r,y)=>p&&r.length==w&(y==w>>1|++y%w<2?/^[MS]+$/:/^[MS]( *)[MS]\1[MS]$/).test(r),w=s.length)

Test cases

let f =

s=>s.reduce((p,r,y)=>p&&r.length==w&(y==w>>1|++y%w<2?/^[MS]+$/:/^[MS]( *)[MS]\1[MS]$/).test(r),w=s.length)

console.log('Testing truthy test cases...');

console.log(f([
  'MMM',
  'MMM',
  'MMM'
]));

console.log(f([
  'SMSMS',
  'M M M',
  'SMSMS',
  'M M M',
  'SMSMS'
]));

console.log(f([
  'MMMMMMM',
  'M  S  M',
  'M  S  M',
  'MSSSSSM',
  'M  S  M',
  'M  S  M',
  'MMMMMMM'
]));

console.log('Testing falsy test cases...');

console.log(f([
  'Hello, World!'
]));

console.log(f([
  'MMMM',
  'MSSM',
  'MS M',
  'MMMM'
]));

console.log(f([
  'MMSMM',
  'M S.M',
  'sSSSS',
  'M S M',
  'MMSMM'
]));

console.log(f([
  'MMMMMMM',
  'M  M  M',
  'MMMMMMM',
  'M  M  M',
  'MMMMMMM'
]));

console.log(f([
  'MMMMMMM',
  'M M M M',
  'MMMMMMM',
  'M M M M',
  'MMMMMMM',
  'M M M M',
  'MMMMMMM'
]));

Arnauld

Posted 2017-01-08T05:49:45.603

Reputation: 111 334

2

JavaScript (ES6), 140 138 141 140 bytes

I know this isn't a winning byte count (although thanks to Patrick Roberts for -3, and I realised it threw false positives for 1 instead of M/S: +3), but I did it a slightly different way, I'm new to this, and it was fun...

Accepts an array of strings, one for each line and returns true or false. Newline added for clarity (not included in byte count).

f=t=>t.every((e,i)=>e.split`S`.join`M`==[...p=[b='M'.repeat(s=t.length),
...Array(z=-1+s/2|0).fill([...'MMM'].join(' '.repeat(z)))],...p,b][i])

Instead of checking input against a generalised pattern, I construct an 'M' window of the same size, replace S with M on input, and compare the two.

Ungolfed

f = t => t.every( // function returns true iff every entry in t
                  // returns true below
  (e, i) => e.split`S`.join`M` // replace all S with M
                                 // to compare to mask
  == [ // construct a window of the same size made of Ms and
       // spaces, compare each row 
      ...p = [ // p = repeated vertical panel (bar above pane)
               // which will be repeated
              b = 'M'.repeat(s = t.length),
                  // b = bar of Ms as long as the input array
              ...Array(z = -1 + s/2|0).fill([...'MMM'].join(' '.repeat(z)))],
              // z = pane size; create enough pane rows with
              // Ms and enough spaces
      ...p, // repeat the panel once more
      b][i] // finish with a bar
)

console.log(f(["111","111","111"]))

console.log(f(["MMMMM","M S M","MSSSM","M S M","MSSSM"]))

Test cases

f=t=>t.every((e,i)=>e.split`S`.join`M`==[...p=[b='M'.repeat(s=t.length),
...Array(z=-1+s/2|0).fill([...'MMM'].join(' '.repeat(z)))],...p,b][i])


truthy=`MMM
MMM
MMM

SMSMS
M M M
SMSMS
M M M
SMSMS

MMMMMMM
M  S  M
M  S  M
MSSSSSM
M  S  M
M  S  M
MMMMMMM`.split('\n\n')

falsey=`Hello, World!

MMMM
MSSM
MS M
MMMM

MMSMM
M S.M
sSSSS
M S M
MMSMM

MMMMMMM
M  M  M
MMMMMMM
M  M  M
MMMMMMM

MMMMMMM
M M M M
MMMMMMM
M M M M
MMMMMMM
M M M M
MMMMMMM`.split('\n\n')

truthy.forEach(test=>{
  console.log(test,f(test.split('\n')))
})

falsey.forEach(test=>{
  console.log(test,f(test.split('\n')))
})

Chris M

Posted 2017-01-08T05:49:45.603

Reputation: 191

1For future reference, unless the function is recursive, f= doesn't need to be included in the byte count, so this is actually a 138 byte submission. – Patrick Roberts – 2017-01-08T21:34:29.777

You can replace z=-1+s/2|0 with z=(s-3)/2 to save 1 byte – Patrick Roberts – 2017-01-08T22:36:06.893

You can also replace e.replace(/S/g,'M')==... with e.split`S`.join`M`==... to save another byte – Patrick Roberts – 2017-01-09T00:10:44.210

Thanks! z=-1+s/2|0 is there to return a positive integer for s==1 and even s, i.e. the function returns false without Array() crashing it. Otherwise the necessary logic made it longer. Great tip on split/join, thanks – Chris M – 2017-01-09T07:27:01.227

Good catch, I didn't consider the s=1 case, since my invalid regex just silently fails. – Patrick Roberts – 2017-01-09T08:51:20.000

1

JavaScript (ES6), 109 107 106 105 99 bytes

s=>!s.split`S`.join`M`.search(`^((M{${r=s.search`
`}})(
(M( {${w=(r-3)/2}})M\\5M
){${w}}))\\1\\2$`)

Edit: Whoa, Arnauld saved me 6 bytes by changing s.split`\n`.length to s.search`\n`! Thanks!

This takes a single multiline string and constructs a RegExp-based validation using the length of the input string. Returns true or false. Assumes a valid window has does not have a trailing newline.

Demo

f=s=>!s.split`S`.join`M`.search(`^((M{${r=s.search`
`}})(
(M( {${w=(r-3)/2}})M\\5M
){${w}}))\\1\\2$`);
`MMM
MMM
MMM

SMSMS
M M M
SMSMS
M M M
SMSMS

MMMMMMM
M  S  M
M  S  M
MSSSSSM
M  S  M
M  S  M
MMMMMMM

Hello, World!

MMMM
MSSM
MS M
MMMM

MMSMM
M S.M
sSSSS
M S M
MMSMM

MMMMMMM
M  M  M
MMMMMMM
M  M  M
MMMMMMM

MMMMMMM
M M M M
MMMMMMM
M M M M
MMMMMMM
M M M M
MMMMMMM`.split`

`.forEach(test=>{console.log(test,f(test));});

Patrick Roberts

Posted 2017-01-08T05:49:45.603

Reputation: 2 475

Nice approach! Could you use r=s.search('\n') instead of split / length? – Arnauld – 2017-01-09T07:58:53.163

@Arnauld awesome suggestion, thanks! – Patrick Roberts – 2017-01-09T08:59:13.997

The parenthesys on s=>!s.split`S`.join`M`.search([...]) can be removed, without causing syntax errors. – Ismael Miguel – 2017-01-09T11:50:54.973

@IsmaelMiguel correct, but then the string gets passed as a template, which invalidates the implicit RegExp – Patrick Roberts – 2017-01-09T12:37:14.087

That sucks... I really wasnt expecting that... – Ismael Miguel – 2017-01-09T13:19:41.010

@IsmaelMiguel here's how that syntax works: tagged template literals

– Patrick Roberts – 2017-01-10T09:29:29.547