11
\$\begingroup\$

In the face of some regrettable rules that turned the original into a glorified sorting task, I am posting a more challenging variant. Shout out to Luis Mendo for the suggestion of how to improve the original challenge.


You've inherited a server that runs several apps which all output to the same log.

Your task is to de-interleave the lines of the log file by source. Fortunately for you, the person who wrote all the apps was nice enough to leave tags indicating their source.

Logs

Each line will look something like this:

[app_name] Something horrible happened!
  • App tags are always between square brackets and will contain only alphanumeric characters and underscores.
  • App tags are nonempty
  • There may be other square brackets later on any given line, but none will form a valid tag.
  • There will always be at least one non-space character after a tag.
  • The log as a whole may be empty.
  • There is no limit to how many unique app tags will be present in the file.

In some cases, an app tag may be missing. When this is the case, the log line belongs to the most recently logged app.

  • The first line of the log will always begin with an app tag
  • A line beginning with [ is not necessarily tagged. If there is an invalid character between the initial square brackets or no ], then the line is not tagged.
  • No empty lines appear in the log

Expected Output

You should output several fully-separated logs with the app tags removed from each log line where they were present. You do not need to preserve leading whitespace on any log line.

Output logs must be in some sort of key-value mapping or reasonable equivalent. A non-exhaustive list of valid output formats:

  • A file named after the app tag for each app
    • You may assume that the output files do not already exist in the output directory in this case.
  • A dictionary/map/hash/whatever that uses app tags as keys and a newline-separated string of the log lines as values.
  • A long concatenated string separated by blank lines and preceded by app tags
  • A list of [key, value] lists
  • A JSON string with app tags as keys and arrays of log lines as values
  • A Markdown document with app tags as headers and leading #s of any line escaped with backslashes.
  • A Javascript function that takes a string as input and outputs the associated log as a newline-separated string.

Basically, if you can't tell which app the log lines came from, the output is invalid.

Example

An entire log might look like this:

[weather] Current temp: 83F
[barkeep] Fish enters bar
Fish orders beer
[stockmarket] PI +3.14
[PI announced merger with E]
[barkeep] Fish leaves bar
[weather] 40% chance of rain detected
[ I have a lovely bunch of coconuts

Which should output three different logs:

weather:

Current temp: 83F
40% chance of rain detected
[ I have a lovely bunch of coconuts

barkeep:

Fish enters bar
Fish orders beer
Fish leaves bar

stockmarket:

PI +3.14
[PI announced merger with E]

You are not given the names of the app tags ahead of time. You must determine them only by analyzing the log file.

Rules and Scoring

  • This is , so shortest code wins.
  • Standard rules and loopholes apply
  • Use any convenient IO format, provided that each input line is represented as a string, not a pre-parsed tag + message. Parsing is part of this challenge.
  • Output log lines for each app must appear in the same order that they did in the original log.
  • You may assume that the input log contains only ASCII characters.
\$\endgroup\$
  • \$\begingroup\$ Is input guaranteed to be ASCII-only? \$\endgroup\$ – Adám Aug 24 at 16:39
  • \$\begingroup\$ @Adám you may assume that, yes. Adding now. \$\endgroup\$ – Beefster Aug 24 at 16:40
  • 1
    \$\begingroup\$ Test case suggestion: a tagged line, then an untagged line, then a tagged line with the same tag. \$\endgroup\$ – Zgarb Aug 24 at 18:08
  • 1
    \$\begingroup\$ I voted to close this, based on what was discussed on codegolf.meta.stackexchange.com/questions/3604/… as this still applies. The argument present was that the needed changes were minor, making them basically the same challenge. The heavy lifting was made on the previous post. I present codegolf.stackexchange.com/a/210120 as part of the evidence that the heavy lifting was done on the previous challenge, and that this is a duplicate of codegolf.stackexchange.com/questions/209666/… \$\endgroup\$ – Ismael Miguel Aug 25 at 10:32
  • 1
    \$\begingroup\$ Since you prefer this challenge I've swapped the duplicate. I think the old challenge was marginally better, although I think both are pretty bad and the modification doesn't really solve the the old problem. \$\endgroup\$ – Wheat Wizard Aug 26 at 19:07
2
\$\begingroup\$

Python 3.8, 95 bytes

import re
lambda x:[((t:=re.match(r'\[(\w*)\]',s)or t)[1],s.split(t[0])[-1].strip())for s in x]

Try it online!

(Expanded TIO example with input)

Explanation:

Python 3.8 is required for the := operator. This takes a list of strings as input, and outputs a list of (tag, body) tuples. First, it uses a Regex match to get the tag:

t:=re.match(r'\[(\w*)\]',s)or t)

This matches any initial sequence of word characters (alphanumeric + underscore) enclosed in square brackets, with the words as a capturing group. If the string does match this regex, t will be a match object with two elements: the full match and the group. For example, if the string is [tag] body, the match will have the elements [tag] and tag.

If the string does not match this regex, then re.match() returns None. The code becomes t = None or t, which is just t = t, so the tag keeps its value from the previous line. If the first line didn't have a match, this would cause an error, but we don't need to worry about that!

The code then constructs the tuple t[1], s.split(t[0])[-1].strip(), where t[1] is the capturing group (the tag without square brackets) and t[0] is the tag with square brackets. Splitting the string on the full tag isolates the body, whether or not the tag actually exists in the string.

| improve this answer | |
\$\endgroup\$
2
\$\begingroup\$

Retina 0.8.2, 95 bytes

+m`^(\[\w+] ).*¶(?!\[\w+])
$&$1
O$`(\w+).*
$1
¶
¶¶
rm`(?<=^\1.*¶)¶(.\w+].)

(?<=(^|¶¶).\w+]).
¶

Try it online! Explanation:

+m`^(\[\w+] ).*¶(?!\[\w+])
$&$1

Tag all untagged lines.

O$`(\w+).*
$1

Sort the lines, taken from my answer to the original challenge.

¶
¶¶

Double-space the lines.

rm`(?<=^\1.*¶)¶(.\w+].)

Remove duplicate tags and the empty line before them. This means that the only empty lines left are those that delimit the separate tags.

(?<=(^|¶¶).\w+]).
¶

Move the tag to its own line.

| improve this answer | |
\$\endgroup\$
2
\$\begingroup\$

perl -Mfeature=say -n, 47 46 bytes

(Saved one byte courtesy of @Dom Hastings)

$;=$1 if s/^\[(\w+)\] +//;$;{$;}.=$_}{say for%;

Try it online!

How does this work?

First, the effect of the -n switch. This causes Perl to wrap the program in a loop, which reads the input and executes the body for each line. But it does so in a very unsophisticated way, it wraps the body in the loop before doing any parsing, as follows:

perl -ne 'TEXT'

gets turned into

LINE: while (defined($_ = readline ARGV)) {
    TEXT;
}

But that means if your TEXT is of the form LOOP_BODY}{FINAL_STATEMENT, you end up with the program:

LINE: while (defined($_ = readline ARGV)) {
    LOOP_BODY
}
{
    FINAL_STATEMENT;
}

We're using this trick just to save a few bytes over an END block.

In the program itself, we're using two variables to do our bookkeeping. $; will contain the current tag, and in the hash %;, we track the lines for each tag. Now, for each line of the input, we check to see if it starts with a tag, and if so, we strip it off from the line, and remember the tag:

$; = $1 if          # Remember the tag if,
s/^\[(\w+)\] +//;   # we can strip of a tag

We then concatenate the current line (stripped from a tag, if present) to the set of lines already collected for that tag -- if there are no such lines, we effectively concatenate it with the empty string:

$;{$;}.=$_   # Remember the current line

Finally, after reading all lines, we print the hash. Perl conveniently flattens a hash to a simple list if you treat it as a list, alternating the keys and values. This gives us output where each section is separated by a newline, and is headed by the tag.

say for%;    # Print the flattened hash
| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ You know you want to drop that final ;... :P \$\endgroup\$ – Dom Hastings Sep 1 at 14:48
  • 1
    \$\begingroup\$ @Abigail: I think that I learned from your own previous replies to me that one of the reasons for using the (otherwise unintuitive) $; variable is that you can leave-out the ; at the end of programs like this...? So saving one bite, right? \$\endgroup\$ – Dominic van Essen Sep 1 at 14:50
  • \$\begingroup\$ @DomHastings Oh, good point. \$\endgroup\$ – Abigail Sep 1 at 14:51
  • \$\begingroup\$ ...and presumably the reason for the second + is to tidy-up extra indentation after the tag, but do you need to do that? \$\endgroup\$ – Dominic van Essen Sep 1 at 15:02
  • \$\begingroup\$ @DominicvanEssen I don't think it's given there's only a single space after the closing bracket. \$\endgroup\$ – Abigail Sep 1 at 15:07
1
\$\begingroup\$

05AB1E, 22 bytes

vyD']¡н¦DžjÃÊi‚»]).¡#н

Input as a list of lines, output as a list of lists of multi-line strings.

Try it online (pretty-printed; feel free to remove the footer to see the actual output).

Explanation:

v                 # Loop `y` over each string of the (implicit) input-list:
 yD               #  Push line `y` twice
   ']¡           '#  Split the copy on "]"
      н           #  Only leave the first part
       ¦          #  Remove the leading character (the potential "[")
        D         #  Duplicate it
         žj       #  Push builtin string "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"
           Ã      #  Only keep those characters in the string we duplicated
            Êi    #  If it is NOT equal to the string:
              ‚   #   Pair it with the previous line
               »  #   And join that pair with a newline delimiter
]                 # Close both the if-statement and loop
 )                # Wrap all values on the stack into a list
  .¡              # Group all strings by:
    #             #  Split the string on spaces
     н            #  And only leave the first part (the tag)
                  # (after which the result is output implicitly)
| improve this answer | |
\$\endgroup\$
1
\$\begingroup\$

AWK-F], 122 123 113 bytes

Added a byte to fix a bug kindly pointed out by water_ghosts.

Saved 10 bytes thanks to Giuseppe!!!

/^\[\w+\]/{a[l=$1][i++]=$2;next}{a[l][i++]=$0}END{for(k in a){print"\n",substr(k,2);for(j in a[k])print a[k][j]}}

Try it online!

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ This doesn't work for tags with underscores \$\endgroup\$ – water_ghosts Aug 24 at 18:42
  • \$\begingroup\$ @water_ghosts Fixed that - thanks! :D \$\endgroup\$ – Noodle9 Aug 24 at 18:51
  • \$\begingroup\$ I don't know AWK but I think you should be able to use \w in place of [A-Za-z0-9_] \$\endgroup\$ – Giuseppe Aug 24 at 21:58
  • \$\begingroup\$ @Giuseppe Nice one - thanks! :D \$\endgroup\$ – Noodle9 Aug 24 at 22:12
1
\$\begingroup\$

SimpleTemplate, 142 bytes

Well, this wasn't too hard.

This answer is a slightly modified version of: De-interleave log lines

{@callexplode intoL EOL,argv.0}{@eachL}{@if_ matches"@^(\[\w+\]) ?(.+)$@"M}{@setX"#{M.1} "}{@set_ M.2}{@/}{@setS.[X]S.[X],X,_,EOL}{@/}{@echoS}

Ungolfed:

Since this is quite unreadable, below is a readable version:

{@call explode into lines EOL, argv.0}
{@set storage null}
{@each lines as line}
    {@if line matches "@^(\[\w+\]) ?(.+)$@" match}
        {@set last "#{match.1} "}
        {@set line match.2}
    {@/}
    {@set storage.[last] storage.[last], last, line, EOL}
{@/}
{@echo storage}

Changes:

Some changes had to be done to work properly, with the new requirements. Below is a copy of the linked answer:

{@call explode into lines EOL, argv.0}
{@set storage null}
{@each lines as line}
    {@if line matches "@^(\[.*\])@" match}
        {@set storage.[match.1] storage.[match.1], line, EOL}
    {@/}
{@/}
{@echo storage}

Below is a full list of the changes:

  • The regular expression was changed to match the remaining content, without a space, if present. (Quoting: "There will always be at least one non-space character after a tag.")
  • Stores the "app" with an extra space, for later use and to normalize the lines (which may or may not have a space right after the "tag").
  • Stores the remaining content, without the first space, into the line (_ for the golfed version) variable
  • Adds the "tag" before the line variable, which used to be part of the line variable.

As you can see, the changes aren't that significant. Move code, add extra space, add a variable to an output.


You can try this on: http://sandbox.onlinephpfunctions.com/code/eb5380ba1826530087fd92fa71d709c0b2d6de39

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

Scala, 127 bytes

l=>((("",List[(String,String)]())/:l){case((p,m),s"[$t] $b")=>(t,(t,b)::m)case((p,m),b)=>(p,(p,b)::m)})._2.groupMap(_._1)(_._2)

Try it in Scastie (doesn't work in TIO)

Wow, this is long.

| improve this answer | |
\$\endgroup\$

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.