{ }codelper
文字の添字文字列の連結P0144

Web アドレスのパスを整理する

一行目にアドレスの数 n が与えられ、続いて Web アドレスのパス部分が一行に一つずつ n 個与えられる。
アドレスごとにパスを整理し、一行に一つずつ出力せよ。
アドレスに # があれば、最初に出る # から最後までがフラグメントである。
フラグメントを除いた前の部分に ?
があれば、最初に出る ?
からその前の部分の終わりまでがクエリである。
残りの先頭部分がパスである。
したがってフラグメントの中の ?
はクエリではなく、クエリやフラグメントの中の / と .
はパスではない。
パスを / があるところごとに区切った一つ一つの部分を名前と呼び、先頭から順に次のように処理する。
空の名前("//" や先頭・末尾の / のためにできたもの)と "." は捨てる。
".." はこれまで残った名前のうち最後の一つを消す。
残った名前がなければ何もしない。
それ以外の名前("..."、"..a"、".git" を含む)はそのまま残す。
残った名前を / でつなぎ、先頭に / を付けたものが整理したパスである(残った名前がなければ "/")。
パスの最後の名前(最後の / の後ろの部分)が空の名前、"."、".." のどれかなら、そのアドレスはフォルダを指す。
このとき残った名前が一つ以上あれば、整理したパスの末尾に / を一つ付ける。
整理したパスの後にクエリとフラグメントを元のまま付けて出力する。

入力
1 ≤ n ≤ 100。
アドレスは / で始まり、長さ 1〜200 で、英小文字、数字、そして .
- _ / ?
# = & だけからなる
出力
整理したアドレス n 行
例 1 · stdin
4
/a/./b/../c/
/../x//y
/docs/v1/../v2/index.html?lang=ko#top
/a/b?next=/../c#x?y
stdout
/a/c/
/x/y
/docs/v2/index.html?lang=ko#top
/a/b?next=/../c#x?y
例 2 · stdin
5
/a/b#x/../y?z
/./.../..a/.
/a/b/..
/a/..
/
stdout
/a/b#x/../y?z
/.../..a/
/a/
/
/
main.py
入力(stdin)

実行すると、これらの行が順に input()・標準入力に渡されます(実行中に入力を求めません)。

$ 準備完了

コードを入力して解くにはパソコンで開いてください。モバイルでは問題と解答を見られます。

ほかの言語では?言語別の解答を見る · 8 言語の解答

落とし穴は五つです。
一つ目、?
を文字列全体から先に探すと、"/a/b#x/../y?z" でフラグメントの中の ?
をクエリの始まりとみなし、フラグメントの x/../y までパスとして整理してしまいます。
# を先に探し、?
はその前の部分だけで探す必要があります。
最初に出る位置を探すのは Python find(なければ -1、index は例外)、JS・TS・Java indexOf、C# IndexOf、C strchr(なければ NULL)、C++ find(なければ npos)、Craykon 位置検索(なければ -1)です。
二つ目、クエリ・フラグメントを除かずに丸ごと分けると、"?next=/../c" の .. までパスとして処理します。
三つ目、一番上で .. に出会う場合です。
Python list.pop() は空のリストで IndexError、Java remove(size() - 1) は IndexOutOfBoundsException、C++ の空の vector の pop_back() は未定義動作なので、先に空かどうかを聞く必要があります。
JS・TS pop() は空の配列でただ undefined を返すので、聞かなくても大丈夫です。
四つ目、点で始まるかだけを見ると "..." や ".git" も消します。
名前全体が . や .. と同じかを見る必要があります。
五つ目、末尾の / をいつも取ると "/a/c/" が "/a/c" になります。
パスが / で終わるかだけを見ても足りません。"/a/b/.." のように . や .. で終わってもフォルダを指すので "/a/" です。
分けるときの空の名前の扱いは言語ごとに違います。
Python split("/")、JS・TS split("/")、Craykon 分ける("/") は空の名前を残し、Java split("/") は末尾の空の名前だけ捨て、C# Split('/', StringSplitOptions.RemoveEmptyEntries) と C strtok はすべて捨てます。
そのため Java・C#・C では分けた結果の末尾を見ると "/a/" の空の名前を見落とします。
最後の名前は最後の / の後ろを切り出して見ます(lastIndexOf('/')、C strrchr、C++ rfind)。
つなぐのは Python "/".join(stack)、JS・TS stack.join("/")、Java String.join("/", stack)、C# string.Join("/", stack)、Craykon stack.つなぐ("/") で、C・C++ にはないので最初の名前の前だけ除いて / を付けながら書きます。

Pythonmain.py · 26 行
def normalize(url):    # 조각(#)을 먼저 떼야 한다: 조각 안의 ?는 질의가 아니다    h = url.find("#")    rest, frag = (url, "") if h == -1 else (url[:h], url[h:])    q = rest.find("?")    path, query = (rest, "") if q == -1 else (rest[:q], rest[q:])    segs = path.split("/")    stack = []    for seg in segs:        if seg == "" or seg == ".":            continue        if seg == "..":            if stack:  # 맨 위에서는 더 올라가지 않는다                stack.pop()        else:            stack.append(seg)    clean = "/" + "/".join(stack)    # 마지막 이름이 빈 이름·.·.. 이면 폴더를 가리키니 끝의 / 를 남긴다    if stack and segs[-1] in ("", ".", ".."):        clean += "/"    return clean + query + frag  n = int(input())for _ in range(n):    print(normalize(input()))
JavaScriptmain.js · 25 行
const lines = require("fs").readFileSync(0, "utf8").split("\n");const n = Number(lines[0]); function normalize(url) {  // 조각(#)을 먼저 떼야 한다: 조각 안의 ?는 질의가 아니다  const h = url.indexOf("#");  const rest = h === -1 ? url : url.slice(0, h);  const frag = h === -1 ? "" : url.slice(h);  const q = rest.indexOf("?");  const path = q === -1 ? rest : rest.slice(0, q);  const query = q === -1 ? "" : rest.slice(q);  const stack = [];  for (const seg of path.split("/")) {    if (seg === "" || seg === ".") continue;    if (seg === "..") stack.pop(); // 빈 배열의 pop()은 오류 없이 undefined    else stack.push(seg);  }  let clean = "/" + stack.join("/");  // 마지막 이름이 빈 이름·.·.. 이면 폴더를 가리키니 끝의 / 를 남긴다  const last = path.slice(path.lastIndexOf("/") + 1);  if (stack.length > 0 && (last === "" || last === "." || last === "..")) clean += "/";  return clean + query + frag;} console.log(lines.slice(1, 1 + n).map(normalize).join("\n"));
TypeScriptmain.ts · 26 行
import { readFileSync } from "node:fs";const lines: string[] = readFileSync(0, "utf8").split("\n");const n: number = Number(lines[0]); function normalize(url: string): string {  // 조각(#)을 먼저 떼야 한다: 조각 안의 ?는 질의가 아니다  const h: number = url.indexOf("#");  const rest: string = h === -1 ? url : url.slice(0, h);  const frag: string = h === -1 ? "" : url.slice(h);  const q: number = rest.indexOf("?");  const path: string = q === -1 ? rest : rest.slice(0, q);  const query: string = q === -1 ? "" : rest.slice(q);  const stack: string[] = [];  for (const seg of path.split("/")) {    if (seg === "" || seg === ".") continue;    if (seg === "..") stack.pop(); // 빈 배열의 pop()은 오류 없이 undefined    else stack.push(seg);  }  let clean: string = "/" + stack.join("/");  // 마지막 이름이 빈 이름·.·.. 이면 폴더를 가리키니 끝의 / 를 남긴다  const last: string = path.slice(path.lastIndexOf("/") + 1);  if (stack.length > 0 && (last === "" || last === "." || last === "..")) clean += "/";  return clean + query + frag;} console.log(lines.slice(1, 1 + n).map(normalize).join("\n"));
JavaMain.java · 39 行
import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;import java.util.ArrayList;import java.util.List; public class Main {    static String normalize(String url) {        // 조각(#)을 먼저 떼야 한다: 조각 안의 ?는 질의가 아니다        int h = url.indexOf('#');        String rest = h == -1 ? url : url.substring(0, h);        String frag = h == -1 ? "" : url.substring(h);        int q = rest.indexOf('?');        String path = q == -1 ? rest : rest.substring(0, q);        String query = q == -1 ? "" : rest.substring(q);        List<String> stack = new ArrayList<>();        for (String seg : path.split("/")) {            if (seg.isEmpty() || seg.equals(".")) continue;            if (seg.equals("..")) {                if (!stack.isEmpty()) stack.remove(stack.size() - 1);            } else {                stack.add(seg);            }        }        String clean = "/" + String.join("/", stack);        // 마지막 이름이 빈 이름·.·.. 이면 폴더를 가리키니 끝의 / 를 남긴다        String last = path.substring(path.lastIndexOf('/') + 1);        if (!stack.isEmpty() && (last.isEmpty() || last.equals(".") || last.equals(".."))) clean += "/";        return clean + query + frag;    }     public static void main(String[] args) throws IOException {        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));        int n = Integer.parseInt(in.readLine().trim());        StringBuilder out = new StringBuilder();        for (int i = 0; i < n; i++) out.append(normalize(in.readLine())).append('\n');        System.out.print(out);    }}
C#Main.cs · 37 行
using System;using System.Collections.Generic; class Program{    static string Normalize(string url)    {        // 조각(#)을 먼저 떼야 한다: 조각 안의 ?는 질의가 아니다        int h = url.IndexOf('#');        string rest = h == -1 ? url : url.Substring(0, h);        string frag = h == -1 ? "" : url.Substring(h);        int q = rest.IndexOf('?');        string path = q == -1 ? rest : rest.Substring(0, q);        string query = q == -1 ? "" : rest.Substring(q);        var stack = new List<string>();        foreach (string seg in path.Split('/', StringSplitOptions.RemoveEmptyEntries))        {            if (seg == ".") continue;            if (seg == "..")            {                if (stack.Count > 0) stack.RemoveAt(stack.Count - 1);            }            else stack.Add(seg);        }        string clean = "/" + string.Join("/", stack);        // 마지막 이름이 빈 이름·.·.. 이면 폴더를 가리키니 끝의 / 를 남긴다        string last = path.Substring(path.LastIndexOf('/') + 1);        if (stack.Count > 0 && (last == "" || last == "." || last == "..")) clean += "/";        return clean + query + frag;    }     static void Main()    {        int n = int.Parse(Console.ReadLine());        for (int i = 0; i < n; i++) Console.WriteLine(Normalize(Console.ReadLine()));    }}
C++main.cpp · 41 行
#include <iostream>#include <sstream>#include <string>#include <vector> std::string normalize(const std::string& url) {    // 조각(#)을 먼저 떼야 한다: 조각 안의 ?는 질의가 아니다    size_t h = url.find('#');    std::string rest = url.substr(0, h);  // h가 npos면 끝까지    std::string frag = h == std::string::npos ? "" : url.substr(h);    size_t q = rest.find('?');    std::string path = rest.substr(0, q);    std::string query = q == std::string::npos ? "" : rest.substr(q);    std::vector<std::string> stack;    std::stringstream ss(path);    std::string seg;    while (std::getline(ss, seg, '/')) {        if (seg.empty() || seg == ".") continue;        if (seg == "..") {            if (!stack.empty()) stack.pop_back();  // 빈 vector의 pop_back은 정의되지 않은 동작        } else {            stack.push_back(seg);        }    }    std::string out = "/";    for (size_t k = 0; k < stack.size(); k++) {        if (k > 0) out += '/';        out += stack[k];    }    // 마지막 이름이 빈 이름·.·.. 이면 폴더를 가리키니 끝의 / 를 남긴다    std::string last = path.substr(path.rfind('/') + 1);    if (!stack.empty() && (last.empty() || last == "." || last == "..")) out += '/';    return out + query + frag;} int main() {    std::string line;    std::getline(std::cin, line);    int n = std::stoi(line);    for (int i = 0; i < n && std::getline(std::cin, line); i++) std::cout << normalize(line) << '\n';}
Cmain.c · 42 行
#include <stdio.h>#include <string.h> int main(void) {    int n;    char url[256];    if (!fgets(url, sizeof url, stdin)) return 0;    sscanf(url, "%d", &n);    for (int i = 0; i < n; i++) {        if (!fgets(url, sizeof url, stdin)) break;        url[strcspn(url, "\n")] = '\0';        /* 조각(#)을 먼저 찾아야 한다: 조각 안의 ?는 질의가 아니다 */        char *h = strchr(url, '#');        char frag[256] = "";        if (h) { strcpy(frag, h); *h = '\0'; }        char *q = strchr(url, '?');        char query[256] = "";        if (q) { strcpy(query, q); *q = '\0'; }        /* 마지막 이름이 빈 이름·.·.. 이면 폴더를 가리킨다(strtok이 url을 바꾸기 전에 본다) */        const char *last = strrchr(url, '/') + 1;        int dirLike = strcmp(last, "") == 0 || strcmp(last, ".") == 0 || strcmp(last, "..") == 0;        /* 이제 url에는 경로만 남았다. strtok은 빈 조각("//")을 저절로 건너뛴다 */        char *stack[128];        int top = 0;        for (char *seg = strtok(url, "/"); seg; seg = strtok(NULL, "/")) {            if (strcmp(seg, ".") == 0) continue;            if (strcmp(seg, "..") == 0) {                if (top > 0) top--;            } else {                stack[top++] = seg;            }        }        char out[512] = "/";        for (int k = 0; k < top; k++) {            if (k > 0) strcat(out, "/");            strcat(out, stack[k]);        }        if (top > 0 && dirLike) strcat(out, "/");        printf("%s%s%s\n", out, query, frag);    }    return 0;}
Craykonmain.cray · 54 行
n = コンソール.行読み().数に();繰り返す (i = 0; i < n; i++){    コンソール.書く(normalize(コンソール.行読み()));} normalize(url){    // 조각(#)을 먼저 떼야 한다: 조각 안의 ?는 질의가 아니다    rest = url;    frag = "";    h = url.位置検索("#");    もし (h != -1)    {        rest = url.切り出す(0, h);        frag = url.切り出す(h);    }    path = rest;    query = "";    q = rest.位置検索("?");    もし (q != -1)    {        path = rest.切り出す(0, q);        query = rest.切り出す(q);    }    segs = path.分ける("/");    stack = [];    それぞれ (seg の中 segs)    {        もし (seg == "" || seg == ".")        {            次へ;        }        もし (seg == "..")        {            もし (stack.個数 > 0)            {                stack.位置で取り除く(stack.個数 - 1);            }        }        でなければ        {            stack.追加(seg);        }    }    clean = "/" + stack.つなぐ("/");    // 마지막 이름이 빈 이름·.·.. 이면 폴더를 가리키니 끝의 / 를 남긴다    last = segs[segs.個数 - 1];    もし (stack.個数 > 0 && (last == "" || last == "." || last == ".."))    {        clean += "/";    }    返す clean + query + frag;}