28
\$\begingroup\$

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
\$\endgroup\$
  • 3
    \$\begingroup\$ This is a great twist on ASCII arts, a decision problem to detect a certain structure. \$\endgroup\$ – xnor Jan 8 '17 at 5:53
  • 4
    \$\begingroup\$ @xnor I feel like we might want a different tag for reverse ASCII art like this. \$\endgroup\$ – Esolanging Fruit Jan 8 '17 at 5:55
  • 2
    \$\begingroup\$ while not specific to ascii art, pattern matching might be a good choice for a new tag \$\endgroup\$ – Destructible Lemon Jan 8 '17 at 6:15
  • \$\begingroup\$ Can you add a test case or two where the string doesn't form a rectangular array? \$\endgroup\$ – Greg Martin Jan 8 '17 at 8:54
  • 1
    \$\begingroup\$ @Mast, you are quite right! Maybe the challenge needs clarifying \$\endgroup\$ – Chris M Jan 8 '17 at 17:19
1
\$\begingroup\$

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")
\$\endgroup\$
8
\$\begingroup\$

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!

\$\endgroup\$
7
\$\begingroup\$

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).

\$\endgroup\$
5
\$\begingroup\$

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.

\$\endgroup\$
  • \$\begingroup\$ You can change (...).includes(c) to ~(...).search(c) to save 1 byte \$\endgroup\$ – Patrick Roberts Jan 8 '17 at 21:15
  • 1
    \$\begingroup\$ Actually, even better you can change it to (...?/ /:/[MS]/).test(c) to save 2 bytes instead of just 1. \$\endgroup\$ – Patrick Roberts Jan 8 '17 at 21:24
  • \$\begingroup\$ @PatrickRoberts Cute, thanks! \$\endgroup\$ – Neil Jan 8 '17 at 21:28
5
\$\begingroup\$

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=<>
\$\endgroup\$
  • \$\begingroup\$ 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..--$. \$\endgroup\$ – Dada Jan 8 '17 at 12:44
  • \$\begingroup\$ @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. :-) \$\endgroup\$ – nwk Jan 8 '17 at 22:38
  • \$\begingroup\$ Make that 26 characters. \$\endgroup\$ – nwk Jan 8 '17 at 22:44
  • 1
    \$\begingroup\$ Actually, 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 !~. \$\endgroup\$ – Dada Jan 9 '17 at 6:57
  • \$\begingroup\$ 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. \$\endgroup\$ – Dada Jan 9 '17 at 8:05
2
\$\begingroup\$

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.

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

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'
]));

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

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')))
})

\$\endgroup\$
  • 1
    \$\begingroup\$ For 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. \$\endgroup\$ – Patrick Roberts Jan 8 '17 at 21:34
  • \$\begingroup\$ You can replace z=-1+s/2|0 with z=(s-3)/2 to save 1 byte \$\endgroup\$ – Patrick Roberts Jan 8 '17 at 22:36
  • \$\begingroup\$ You can also replace e.replace(/S/g,'M')==... with e.split`S`.join`M`==... to save another byte \$\endgroup\$ – Patrick Roberts Jan 9 '17 at 0:10
  • \$\begingroup\$ 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 \$\endgroup\$ – Chris M Jan 9 '17 at 7:27
  • \$\begingroup\$ Good catch, I didn't consider the s=1 case, since my invalid regex just silently fails. \$\endgroup\$ – Patrick Roberts Jan 9 '17 at 8:51
1
\$\begingroup\$

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));});

\$\endgroup\$
  • \$\begingroup\$ Nice approach! Could you use r=s.search('\n') instead of split / length? \$\endgroup\$ – Arnauld Jan 9 '17 at 7:58
  • \$\begingroup\$ @Arnauld awesome suggestion, thanks! \$\endgroup\$ – Patrick Roberts Jan 9 '17 at 8:59
  • \$\begingroup\$ The parenthesys on s=>!s.split`S`.join`M`.search([...]) can be removed, without causing syntax errors. \$\endgroup\$ – Ismael Miguel Jan 9 '17 at 11:50
  • \$\begingroup\$ @IsmaelMiguel correct, but then the string gets passed as a template, which invalidates the implicit RegExp \$\endgroup\$ – Patrick Roberts Jan 9 '17 at 12:37
  • \$\begingroup\$ That sucks... I really wasnt expecting that... \$\endgroup\$ – Ismael Miguel Jan 9 '17 at 13:19

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.