Regex (PCRE2), 203 193 177 181 bytes
/((?|^()|^(?:.(?=.*
(\2.)))+){2}(?(8)(?=\8$)(?|()|(?:(?6)(?=.+((?6)\3)\7$))++){2}((?6)|(?!\5)7())\3(?=\7$)|o([^o7_])+o)(?|()|\7(?:.(?=.*
.*(.\7)))+){2}
(?=\2(.*)))+\5.*(?=\8)_+\7$/m
Try it on regex101
Explanation
((?|^()|^(?:.(?=.*\n(\2.)))+){2}(?(8)(?=\8$)(?|()|(?:(?6)(?=.+((?6)\3)\7$))++){2}((?6)|(?!\5)7())\3(?=\7$)|o([^o7_])+o)(?|()|\7(?:.(?=.*\n.*(.\7)))+){2}\n(?=\2(.*)))+
Main loop. Matches everything except for the mouth:
(?|^()|^(?:.(?=.*\n(\2.)))+){2}
- on each line, match the padding to the left of the face, while counting a matching number of characters of padding into \2
from the start of the next line. This needs to use a Branch Reset Group (?|
...)
in order to initialize \2
to empty with ^()
before building it up one character at a time with (\2.)
. The {2}
guarantees that both stages will execute exactly once, and do so in the correct order due to each one having a ^
anchor (which matches the start of any line due to the /m
flag). Replacing the {2}
with +
wouldn't work because a loop will immediately terminate after any optional iteration that matches zero characters. Note that in the case of zero padding, the first stage will execute twice in a row.
(?(8)
...|
...eyes...)
- Only allow matching the eyes if we haven't already captured \8
, i.e. iterated through the main loop at least once.
(?!\7)o([^o7_])+o
- match the eyes, and define the subroutine (?6)
to do [^o7_]
.
(?=\8$)(?|()|(?:(?6)(?=.+((?6)\3)\7$))++){2}((?6)|(?!\5)7())\3(?=\7$)
- Match the facial part of a row either containing no facial features, or containing the nose.
(?=\8$)
- assert that we're starting at the proper column, which was marked when the previous row was processed
(?|()|(?:(?6)(?=.+((?6)\3)\7$))++){2}
- build up the facial part of the row one character at a time on the left and right ends. The right part is built up in \3
. Both ends a built towards the center. The ++
ensures this is done the maximum number of times; with an even length both ends would meet, and with an odd length one character is left between them.
This uses a Branch Reset Group and {2}
the same way as before, except that we guarantee both stages execute in the correct order differently. The ++
guarantees the second stage won't execute twice. The fact that an empty match would result in (?=\7$)
failing to match guarantees that the first stage executes first. The fact that executing the two stages in the wrong order would result in too-long row (with the previous value of \3
tacked in) guarantees the stages execute in the correct order.
((?6)|(?!\5)7())
- match either the absence of a nose or a nose. (?6)
matches the absence of a nose, and (?!\5)7()
matches a nose; ()
here sets \5
to mark the fact that we've matched a nose, and (?!\5)
prevents matching a nose if we've already done so.
\3
- match the right half of the facial part of the row, which we built up.
(?=\7$)
- assert that we're now at the first column directly to the right of the face.
(?|()|\7(?:.(?=.*\n.*(.\7)))+){2}
- match the padding to the right of the facial part of this line, while counting a matching number of characters of padding into \7
from the corresponding part of the next line. Uses the same Branch Reset Group {2}
trick as before, except that we guarantee both stages execute in the correct order in yet another different way. Sticking a \7
in front of the second stage forces it to match too many characters if it tries to execute the second stage first, because the previous value of \7
will be concatenated to the desired number of characters, which would only match in invalid input that has a line longer than the previous line. And the subsequent \n
(seen below) prevents the first stage from executing twice on any valid input, because that would only match on a line shorter than the previous line. Note that in the case of zero padding, the first stage will execute twice in a row.
\n(?=\2(.*))
- match the end of the current line, and capture a marker for the desired starting column of the facial part of the next row in \8
.
Upon finishing the main loop, we reach \5.*(?=\8)_+\7$
:
\5
- assert that we matched a nose.
.*(?=\8)
- skip to the correct starting column, as marked when the previous line was processed.
_+
- match the mouth.
\7$
- assert that the mouth ended at the proper column.
Regex (PCRE2), 363 322 283 199 182 184 186 bytes
This version goes above and beyond the problem statement, allowing the input to have variable-length lines.
/((?|^()|^(?:.(?=.*
(\2.)))+){2}(?=(?(8)(?=\8\9)(?|()|(?:(?6)(?=.+((?6)\3)\9))++){2}((?6)|(?!\5)7())\3(?=\9)|o([^o7_
])+o)(.*))(?|()|\8(?:.(?=.*
\2(\8.)(\C*)))+){2}\7
)+\5.*(?=\8)_+\9$/m
Try it on regex101
Explanation
((?|^()|^(?:.(?=.*\n(\2.)))+){2}(?=(?(8)(?=\8\9)(?|()|(?:(?6)(?=.+((?6)\3)\9))++){2}((?6)|(?!\5)7())\3(?=\9)|o([^o7_\n])+o)(.*))(?|()|\8(?:.(?=.*\n\2(\8.)(\C*)))+){2}\7\n)+
Main loop. Matches everything except for the mouth:
(?|^()|^(?:.(?=.*\n(\2.)))+){2}
- on each line, match the padding to the left of the face, while counting a matching number of characters of padding into \2
from the start of the next line. This needs to use a Branch Reset Group (?|
...)
in order to initialize \2
to empty with ^()
before building it up one character at a time with (\2.)
. The {2}
guarantees that both stages will execute exactly once, and do so in the correct order due to each one having a ^
anchor (which matches the start of any line due to the /m
flag). Replacing the {2}
with +
wouldn't work because a loop will immediately terminate after any optional iteration that matches zero characters. Note that in the case of zero padding, the first stage will execute twice in a row.
(?(8)
...|
...eyes...)
- Only allow matching the eyes if we haven't already captured \8
, i.e. iterated through the main loop at least once.
o([^o7_\n])+o
- match the eyes, and define the subroutine (?6)
to do [^o7_\n]
. We need to explicitly exclude \n
because otherwise the first row of the facing (containing the eyes) could be matched with newline(s) included between the eyes, which if orchestrated just right, could result in fooling the rest of the algorithm into matching a face. An alternative way of preventing this would be to capture \7
using (\C*)
instead of (.*)
(which a previous version did), i.e. capturing everything to the end of the string (including newlines) instead of just to the end of the current line, but this results in worse golf, because we would also need to do (?=\7).*\n
instead of \7\n
, costing 6 bytes total.
(?=\8\9)(?|()|(?:(?6)(?=.+((?6)\3)\9))++){2}((?6)|(?!\5)7())\3(?=\9)
- Match the facial part of a row either containing no facial features, or containing the nose. (Do this in a lookahead so that we can iterate a second time over the same characters, in order to count them.)
(?=\8\9)
- assert that we're starting at the proper column, which was marked when the previous row was processed
(?|()|(?:(?6)(?=.+((?6)\3)\9))++){2}
- build up the facial part of the row one character at a time on the left and right ends. The right part is built up in \3
. Both ends a built towards the center. The ++
ensures this is done the maximum number of times; with an even length both ends would meet, and with an odd length one character is left between them.
This uses a Branch Reset Group and {2}
the same way as before, except that we guarantee both stages execute in the correct order differently. The ++
guarantees the second stage won't execute twice. The fact that an empty match would result in (?=\9)
failing to match guarantees that the first stage executes first. The fact that executing the two stages in the wrong order would result in too-long row (with the previous value of \3
tacked in) guarantees the stages execute in the correct order.
((?6)|(?!\5)7())
- match either the absence of a nose or a nose. (?6)
matches the absence of a nose, and (?!\5)7())
matches a nose; ()
here sets \5
to mark the fact that we've matched a nose, and (?!\5)
prevents matching a nose if we've already done so.
\3
- match the right half of the facial part of the row, which we built up.
(?=\9)
- assert that we're now at the first column directly to the right of the face.
(.*)
- capture a marker \7
for the column directly to the right of the facial part of the current line
(?|()|\8(?:.(?=.*\n\2(\8.)(\C*)))+){2}
- match the facial part of the current line again, this time to count the characters in it, so as to build up a capture in \8
of a matching number of characters from the facial part of the next line, as a marker for column alignment, and capture in \9
a marker for the column directly to the right of the facial part of that line. Uses the same Branch Reset Group {2}
trick as before, and a very similar way of guaranteeing that both stages execute in the correct order. Sticking a \8
in front of the second stage forces it to match too many characters if it tries to execute the second stage first, because the previous value of \8
, not having been emptied by running the first stage, will be concatenated to the desired number of characters; this prevents \7\n
from matching later. And a match can't result from executing the first stage twice – doing so would match an empty row, but the lookahead that matched the facial row the first time already guaranteed that it's of nonzero length.
\7\n
- match the non-facial ending of the current line, ensuring that we ended on the correct column.
Upon finishing the main loop, we reach \5.*(?=\8)_+\9$
:
\5
- assert that we matched a nose.
.*(?=\8)
- skip to the correct starting column, as marked when the previous line was processed.
_+
- match the mouth.
\9$
- assert that the mouth ended at the proper column.
+2 bytes (184 → 186) to fix a bug found by jaytea
L
makes a better nose than a7
, but that means looking down on the face from slightly above right instead of below left. \$\endgroup\$