서버 기록(로그)에서 중요한 줄만 골라 보기 좋게 줄인다.
기록의 단계는 낮은 것부터 DEBUG, INFO, WARN, ERROR 네 가지이고, 이 차례는 풀이에서 상수로 둔다.
첫 줄에 남길 가장 낮은 단계 이름과 기록 수 n이 빈칸 하나로 나뉘어 주어진다.
다음 n줄은 기록 한 줄씩이고, "시각 단계 메시지" 꼴이다. 세 부분 사이는 빈칸 하나다.
메시지 안에는 빈칸이 들어 있을 수 있고, 빈칸 두 개 이상이 이어질 수도 있다. 메시지는 빈칸으로 시작하거나 끝나지 않는다.
단계가 남길 가장 낮은 단계 이상인 기록만 남긴다.
남긴 기록을 차례대로 볼 때, 단계와 메시지가 모두 똑같은 기록이 잇달아 나오면 한 묶음으로 친다. 메시지는 빈칸 수까지 같아야 같은 것이다.
걸러져 버린 기록은 묶음을 끊지 않는다. 남긴 기록 둘 사이에 걸러진 기록만 있으면 두 기록은 잇달아 나온 것이다.
묶음마다 한 줄에 "시각 [단계] 메시지"를 출력하라. 시각은 묶음의 첫 기록의 시각이다.
묶음에 기록이 k개(k ≥ 2)이면 줄 끝에 빈칸 하나와 "(xk)"를 붙인다(k 자리에 수를 쓴다: 3개면 "(x3)"). 기록이 하나뿐인 묶음에는 붙이지 않는다. 메시지는 입력과 글자 하나 다르지 않게 그대로 쓴다.
마지막 줄에는 n개 기록 전체(걸러진 것 포함)를 단계마다 센 개수를 "DEBUG=a, INFO=b, WARN=c, ERROR=d" 꼴로 출력하라.
남긴 기록이 없으면 마지막 줄만 출력한다.
입력
첫 줄 "단계 n"(1 ≤ n ≤ 2000). 시각은 HH:MM:SS(두 자리씩). 단계는 DEBUG·INFO·WARN·ERROR 중 하나. 메시지는 1 ~ 50글자이고 영어 글자·숫자·빈칸·문장 부호(.,:;!?'()=_/#-)로 되어 있다
출력
묶음마다 한 줄 "시각 [단계] 메시지"(2개 이상이면 " (xk)"), 마지막 줄 "DEBUG=a, INFO=b, WARN=c, ERROR=d"
예시 1 · stdin
INFO 7
08:00:01 DEBUG boot sequence start
08:00:02 INFO disk ok
08:00:03 WARN fan slow
08:00:04 DEBUG fan check
08:00:05 WARN fan slow
08:00:09 WARN fan slow
08:00:10 ERROR disk ok
stdout
08:00:02 [INFO] disk ok
08:00:03 [WARN] fan slow (x2)
08:00:09 [WARN] fan slow
08:00:10 [ERROR] disk ok
DEBUG=2, INFO=1, WARN=3, ERROR=1
main.py
오류 목록00F8 ↓ · ⇧F8 ↑
입력(stdin)
실행하면 이 줄들이 차례로 input()·표준 입력으로 들어가요(실행 중에 묻지 않아요).
$ 준비됐어요
코드를 입력해 풀려면 PC에서 열어 주세요. 모바일에서는 문제와 풀이를 볼 수 있어요.
다른 언어로는?언어별 풀이 보기 · 8개 언어 풀이
① 한 줄 읽기와 나누기: 메시지 안의 빈칸을 지키려면 줄을 통째로 읽고 앞의 두 빈칸에서만 나눠요. Python line.split(" ", 2), Java split(" ", 3), C# Split(' ', 3)은 나눌 개수를 정할 수 있어요. JS·TS split(" ", 3)은 뒤를 버리니 indexOf로 자리를 찾아 slice로 자르고, C++ find + substr, C strchr로 빈칸을 '\0'으로 바꿔 세 조각을 만들어요. 크레이콘도 자리찾기와 부분문자열로 잘라요(나누기()는 빈칸마다 나눠요). Python split()으로 다 나눈 뒤 " ".join으로 다시 붙이면 fan slow가 fan slow가 되어 다른 메시지와 묶여요. Java Scanner.next()·nextInt() 뒤에 nextLine()을 부르면 첫 줄의 남은 끝(빈 문자열)을 먼저 읽어서 기록이 하나씩 밀려요. BufferedReader.readLine()으로 줄마다 읽는 게 안전해요. ② 상수 차례: 단계의 높낮이는 이름이 아니라 상수 목록 안의 자리예요. 이름을 문자열로 견주면 글자 차례라 "WARN" > "ERROR"가 되어 ERROR만 남겨야 할 때 WARN도 남아요. 상수 목록은 Python 튜플, JS Object.freeze([...]), TS readonly string[], Java List.of(...)(static final String[]은 이름만 묶고 칸은 바뀌어요), C# static readonly string[](배열은 const가 될 수 없고 칸은 바뀌어요), C static const char *const LEVELS[], C++ constexpr std::array<std::string_view, 4>로 둬요. 크레이콘에는 상수가 없어 바꾸지 않기로 약속한 목록으로 둬요. ③ 이어서 쓰기: Python ", ".join(...), JS·TS join(", "), Java StringJoiner·String.join, C# string.Join, 크레이콘 잇기(", "). C·C++은 첫 개수가 아닐 때만 앞에 ", "를 붙여요. 개수마다 뒤에 붙이면 줄 끝에 ,가 남아요. 묶음은 단계와 메시지를 둘 다 견주고, 걸러진 줄에서 끊지 않아요. 개수 줄은 걸러진 기록까지 모두 세요.
Pythonmain.py · 24 줄
import sys# 차례가 곧 단계의 높이다. 튜플이라 칸을 바꿀 수 없다LEVELS = ("DEBUG", "INFO", "WARN", "ERROR")min_name, n = sys.stdin.readline().split()min_rank = LEVELS.index(min_name)counts = [0] * len(LEVELS)runs = [] # [시각, 단계, 메시지, 개수]for _ in range(int(n)): line = sys.stdin.readline().rstrip("\n") # 앞의 두 빈칸에서만 나눈다: 메시지 안의 빈칸(두 칸 이상도)은 그대로 남는다 time, level, message = line.split(" ", 2) rank = LEVELS.index(level) counts[rank] += 1 if rank < min_rank: continue # 걸러진 줄은 묶음을 끊지 않는다 if runs and runs[-1][1] == level and runs[-1][2] == message: runs[-1][3] += 1 else: runs.append([time, level, message, 1])for time, level, message, k in runs: print(f"{time} [{level}] {message}" + (f" (x{k})" if k > 1 else ""))print(", ".join(f"{name}={c}" for name, c in zip(LEVELS, counts)))
JavaScriptmain.js · 26 줄
// const 배열: 이름은 다시 넣을 수 없고, Object.freeze로 칸도 못 바꾸게 한다const LEVELS = Object.freeze(["DEBUG", "INFO", "WARN", "ERROR"]);const lines = require("fs").readFileSync(0, "utf8").split("\n");const [minName, n] = lines[0].trim().split(/\s+/);const minRank = LEVELS.indexOf(minName);const counts = [0, 0, 0, 0];const runs = []; // { time, level, message, k }for (let i = 1; i <= Number(n); i++) { const line = lines[i]; // 앞의 두 빈칸 자리로 자른다: split(" ")은 메시지 안의 빈칸까지 나눈다 const a = line.indexOf(" "); const b = line.indexOf(" ", a + 1); const time = line.slice(0, a); const level = line.slice(a + 1, b); const message = line.slice(b + 1); const rank = LEVELS.indexOf(level); counts[rank]++; if (rank < minRank) continue; // 걸러진 줄은 묶음을 끊지 않는다 const last = runs[runs.length - 1]; if (last && last.level === level && last.message === message) last.k++; else runs.push({ time, level, message, k: 1 });}const out = runs.map((r) => `${r.time} [${r.level}] ${r.message}` + (r.k > 1 ? ` (x${r.k})` : ""));out.push(LEVELS.map((name, i) => `${name}=${counts[i]}`).join(", "));console.log(out.join("\n"));
TypeScriptmain.ts · 29 줄
import { readFileSync } from "node:fs";// readonly 배열: 타입 검사기가 칸을 바꾸지 못하게 막는다const LEVELS: readonly string[] = ["DEBUG", "INFO", "WARN", "ERROR"];interface Run { time: string; level: string; message: string; k: number }const lines: string[] = readFileSync(0, "utf8").split("\n");const [minName, n] = lines[0].trim().split(/\s+/);const minRank: number = LEVELS.indexOf(minName);const counts: number[] = [0, 0, 0, 0];const runs: Run[] = [];for (let i = 1; i <= Number(n); i++) { const line: string = lines[i]; // 앞의 두 빈칸 자리로 자른다: 메시지 안의 빈칸은 그대로 const a: number = line.indexOf(" "); const b: number = line.indexOf(" ", a + 1); const time = line.slice(0, a); const level = line.slice(a + 1, b); const message = line.slice(b + 1); const rank: number = LEVELS.indexOf(level); counts[rank]++; if (rank < minRank) continue; // 걸러진 줄은 묶음을 끊지 않는다 const last: Run | undefined = runs[runs.length - 1]; if (last !== undefined && last.level === level && last.message === message) last.k++; else runs.push({ time, level, message, k: 1 });}const out: string[] = runs.map((r) => `${r.time} [${r.level}] ${r.message}` + (r.k > 1 ? ` (x${r.k})` : ""));out.push(LEVELS.map((name, i) => `${name}=${counts[i]}`).join(", "));console.log(out.join("\n"));
JavaMain.java · 46 줄
import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;import java.util.ArrayList;import java.util.List;import java.util.StringJoiner;public class Main { // final은 이름만 묶는다: LEVELS[0] = "X"는 막지 않으니 List.of로 바꿀 수 없는 목록을 쓴다 static final List<String> LEVELS = List.of("DEBUG", "INFO", "WARN", "ERROR"); public static void main(String[] args) throws IOException { BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); String[] first = in.readLine().trim().split("\\s+"); int minRank = LEVELS.indexOf(first[0]); int n = Integer.parseInt(first[1]); int[] counts = new int[LEVELS.size()]; List<String[]> runs = new ArrayList<>(); // {시각, 단계, 메시지} List<Integer> sizes = new ArrayList<>(); for (int i = 0; i < n; i++) { // split(" ", 3): 앞의 두 빈칸에서만 나눠 메시지 안의 빈칸은 그대로 남는다 String[] p = in.readLine().split(" ", 3); int rank = LEVELS.indexOf(p[1]); counts[rank]++; if (rank < minRank) continue; // 걸러진 줄은 묶음을 끊지 않는다 int last = runs.size() - 1; if (last >= 0 && runs.get(last)[1].equals(p[1]) && runs.get(last)[2].equals(p[2])) sizes.set(last, sizes.get(last) + 1); else { runs.add(p); sizes.add(1); } } StringBuilder out = new StringBuilder(); for (int i = 0; i < runs.size(); i++) { String[] r = runs.get(i); out.append(r[0]).append(" [").append(r[1]).append("] ").append(r[2]); if (sizes.get(i) > 1) out.append(" (x").append(sizes.get(i)).append(')'); out.append('\n'); } StringJoiner sj = new StringJoiner(", "); for (int i = 0; i < LEVELS.size(); i++) sj.add(LEVELS.get(i) + "=" + counts[i]); out.append(sj).append('\n'); System.out.print(out); }}
C#Main.cs · 37 줄
using System;using System.Collections.Generic;using System.Linq;using System.Text;class Program{ // const는 배열이 될 수 없다: static readonly는 이름만 묶고, 칸은 바꿀 수 있다 static readonly string[] Levels = { "DEBUG", "INFO", "WARN", "ERROR" }; static void Main() { string[] first = Console.ReadLine().Split(' ', StringSplitOptions.RemoveEmptyEntries); int minRank = Array.IndexOf(Levels, first[0]); int n = int.Parse(first[1]); int[] counts = new int[Levels.Length]; var runs = new List<(string Time, string Level, string Message, int K)>(); for (int i = 0; i < n; i++) { // Split(' ', 3): 앞의 두 빈칸에서만 나눠 메시지 안의 빈칸은 그대로 string[] p = Console.ReadLine().Split(' ', 3); int rank = Array.IndexOf(Levels, p[1]); counts[rank]++; if (rank < minRank) continue; // 걸러진 줄은 묶음을 끊지 않는다 int last = runs.Count - 1; if (last >= 0 && runs[last].Level == p[1] && runs[last].Message == p[2]) runs[last] = runs[last] with { K = runs[last].K + 1 }; else runs.Add((p[0], p[1], p[2], 1)); } var sb = new StringBuilder(); foreach (var r in runs) sb.Append($"{r.Time} [{r.Level}] {r.Message}").Append(r.K > 1 ? $" (x{r.K})" : "").Append('\n'); sb.Append(string.Join(", ", Levels.Select((name, i) => $"{name}={counts[i]}"))).Append('\n'); Console.Write(sb); }}
C++main.cpp · 55 줄
#include <algorithm>#include <array>#include <iostream>#include <sstream>#include <string>#include <string_view>#include <vector>// constexpr 배열: 컴파일할 때 정해지고 칸도 바꿀 수 없다constexpr std::array<std::string_view, 4> LEVELS{"DEBUG", "INFO", "WARN", "ERROR"};int rankOf(const std::string& name) { return static_cast<int>(std::find(LEVELS.begin(), LEVELS.end(), name) - LEVELS.begin());}struct Run { std::string time, level, message; int k;};int main() { std::string line; std::getline(std::cin, line); std::istringstream head(line); std::string minName; int n; head >> minName >> n; int minRank = rankOf(minName); std::array<int, 4> counts{}; std::vector<Run> runs; for (int i = 0; i < n; i++) { std::getline(std::cin, line); // 빈칸이 든 한 줄을 통째로 // 앞의 두 빈칸 자리로 자른다: 메시지 안의 빈칸은 그대로 std::size_t a = line.find(' '), b = line.find(' ', a + 1); std::string time = line.substr(0, a), level = line.substr(a + 1, b - a - 1), message = line.substr(b + 1); int rank = rankOf(level); counts[rank]++; if (rank < minRank) continue; // 걸러진 줄은 묶음을 끊지 않는다 if (!runs.empty() && runs.back().level == level && runs.back().message == message) runs.back().k++; else runs.push_back({time, level, message, 1}); } std::string out; for (const Run& r : runs) { out += r.time + " [" + r.level + "] " + r.message; if (r.k > 1) out += " (x" + std::to_string(r.k) + ")"; out += '\n'; } for (std::size_t i = 0; i < LEVELS.size(); i++) { if (i > 0) out += ", "; out += std::string(LEVELS[i]) + "=" + std::to_string(counts[i]); } std::cout << out << '\n';}
Cmain.c · 55 줄
#include <stdio.h>#include <string.h>#define LEVEL_COUNT 4static const char *const LEVELS[LEVEL_COUNT] = {"DEBUG", "INFO", "WARN", "ERROR"};static int rank_of(const char *name) { for (int i = 0; i < LEVEL_COUNT; i++) if (strcmp(LEVELS[i], name) == 0) return i; return -1;}int main(void) { char line[128], min_name[8]; int n; fgets(line, sizeof line, stdin); sscanf(line, "%7s %d", min_name, &n); int min_rank = rank_of(min_name); int counts[LEVEL_COUNT] = {0}; char run_time[9] = "", run_msg[64] = ""; int run_rank = -1, run_k = 0; for (int i = 0; i < n; i++) { fgets(line, sizeof line, stdin); /* 빈칸이 든 한 줄을 통째로 */ line[strcspn(line, "\n")] = '\0'; /* 앞의 두 빈칸을 '\0'으로 바꿔 세 조각으로: 메시지 안의 빈칸은 그대로 */ char *level = strchr(line, ' '); *level++ = '\0'; char *msg = strchr(level, ' '); *msg++ = '\0'; int rank = rank_of(level); counts[rank]++; if (rank < min_rank) continue; /* 걸러진 줄은 묶음을 끊지 않는다 */ if (run_k > 0 && rank == run_rank && strcmp(msg, run_msg) == 0) { run_k++; continue; } if (run_k > 0) { printf("%s [%s] %s", run_time, LEVELS[run_rank], run_msg); if (run_k > 1) printf(" (x%d)", run_k); putchar('\n'); } strcpy(run_time, line); strcpy(run_msg, msg); run_rank = rank; run_k = 1; } if (run_k > 0) { printf("%s [%s] %s", run_time, LEVELS[run_rank], run_msg); if (run_k > 1) printf(" (x%d)", run_k); putchar('\n'); } for (int i = 0; i < LEVEL_COUNT; i++) printf("%s%s=%d", i ? ", " : "", LEVELS[i], counts[i]); putchar('\n'); return 0;}
Craykonmain.cray · 55 줄
// 크레이콘에는 상수가 없다: 바꾸지 않기로 약속한 목록으로 둔다(차례가 곧 높이)단계들 = ["DEBUG", "INFO", "WARN", "ERROR"];첫줄 = 콘솔.줄읽기().나누기();최소 = 단계들.자리찾기(첫줄[0]);n = 첫줄[1].수로변환();개수들 = [0, 0, 0, 0];묶음시각 = [];묶음단계 = [];묶음메시지 = [];묶음개수 = [];반복 (i = 0; i < n; i++){ 줄 = 콘솔.줄읽기(); // 앞의 두 빈칸 자리로 자른다: 나누기()는 메시지 안의 빈칸까지 나눈다 첫빈칸 = 줄.자리찾기(" "); 시각 = 줄.부분문자열(0, 첫빈칸); 나머지 = 줄.부분문자열(첫빈칸 + 1); 둘째빈칸 = 나머지.자리찾기(" "); 단계 = 나머지.부분문자열(0, 둘째빈칸); 메시지 = 나머지.부분문자열(둘째빈칸 + 1); 높이 = 단계들.자리찾기(단계); 개수들[높이] += 1; // 걸러진 줄은 묶음을 끊지 않는다 만약 (높이 >= 최소) { 끝 = 묶음단계.개수 - 1; 만약 (끝 >= 0 && 묶음단계[끝] == 단계 && 묶음메시지[끝] == 메시지) { 묶음개수[끝] += 1; } 아니면 { 묶음시각.넣기(시각); 묶음단계.넣기(단계); 묶음메시지.넣기(메시지); 묶음개수.넣기(1); } }}반복 (j = 0; j < 묶음단계.개수; j++){ 출력줄 = 묶음시각[j] + " [" + 묶음단계[j] + "] " + 묶음메시지[j]; 만약 (묶음개수[j] > 1) { 출력줄 = 출력줄 + " (x" + 묶음개수[j] + ")"; } 콘솔.쓰기(출력줄);}조각들 = [];반복 (j = 0; j < 4; j++){ 조각들.넣기(단계들[j] + "=" + 개수들[j]);}콘솔.쓰기(조각들.잇기(", "));