{ }codelper
文字列の組み立て文字コードP0290

検索条件を付けてリンクを完成させる

検索画面で選んだ条件を基本リンクの後ろに付けて、リンクを一行完成させたい。
一行目に基本リンク base と条件の行数 n が与えられる。
条件の一行は「名前」だけか、「名前 値」のように名前と値が空白一つで区切られている。
行の中で最初に出てくる空白の前が名前で、その後ろ全部が値である。
値の中にはさらに空白があってもよく、空白が続くこともある。
値の中の空白は一つも捨てない。
名前だけの行は値のない条件なので捨てる。
残った条件ごとに、名前、'='、変換した値を順につないで断片を作る。
値は一文字ずつ次の規則で変換する:英大文字・英小文字、数字、そして - _ .
~ の四文字はそのまま残し、それ以外の文字は '%' の後ろにその文字のコードを 16 進数二桁で書いたものに変える。
16 進数の A~F は大文字で書く。
たとえば空白(コード 32)は %20、'/'(コード 47)は %2F になる。
残った条件が一つでもあれば、base の後ろに '?' を付け、断片を与えられた順に '&' でつなぐ。
残った条件がなければ、base だけをそのまま出力する。

入力
一行目に base と n が空白一つで区切って与えられる(1 ≤ n ≤ 100)。
base は空白を含まない 1~50 文字の文字列で、変換せずそのまま使う。
続く n 行に条件が一行ずつ与えられる。
名前は英小文字 1~10 文字である。
値は 1~100 文字で、コード 32~126 の文字(空白、英字、数字、記号)だけからなり、空白で始まったり終わったりしない。
出力
完成したリンク一行
例 1 · stdin
https://shop.example/search 4
q red  shoes & socks
page
sort price_asc
tag 50%/off!
stdout
https://shop.example/search?q=red%20%20shoes%20%26%20socks&sort=price_asc&tag=50%25%2Foff%21
例 2 · stdin
/list 2
page
lang
stdout
/list
main.py
入力(stdin)

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

$ 準備完了

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

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

文字列の断片を集めて間にだけ区切り文字を入れる方法と、文字をコードに変えて 16 進数で書く方法を一緒に使う問題です。
つなぐ:Python "&".join(断片)、JS・TS 断片.join("&")、C# string.Join("&", 断片)、Craykon 断片.つなぐ("&") は間にだけ入れてくれます。
Java StringBuilder や C++ std::string += で一つずつ付けるときは、最初の断片の前に ?、その後は & を自分で選びます。
C はつながずに断片をすぐ出力します。
16 進数二桁:C printf("%%%02X", c)、Java String.format("%%%02X", (int) c)、C# ((int)c).ToString("X2")、Python f"%{ord(c):02X}"、JS c.charCodeAt(0).toString(16).toUpperCase()、C++ と Craykon は "0123456789ABCDEF" から コード / 16 番目と コード % 16 番目の文字を選びます(Craykon は 文字.コード(c) と 数学.商(コード, 16))。
この変換をしてくれる標準関数もありますが、規則が少しずつ違います。
C# Uri.EscapeDataString(値) と Python urllib.parse.quote(値, safe="") はこの問題の規則と同じです。
落とし穴は八つ:① JS encodeURIComponent は !
' ( ) * の五文字をそのまま残します。
② Python quote は既定が safe="/" なので / をそのまま残します。
③ Java URLEncoder.encode は HTML フォームの規則で、空白を + に変え、* はそのまま残し、~ を %7E に変えます。
④ C の %02x は小文字です。
大文字は %02X です。
⑤ 行を名前と値に分けるとき、Java split(" ", 2)、C# Split(' ', 2)、Python split(" ", 1) は残りを最後の断片に残しますが、JS split(" ", 2) は前の二つの断片だけを残して残りを捨てます。
JS では indexOf(" ") と slice で切ります。
⑥ Python split() で単語に分けてから空白一つでつなぎ直すと、続いた空白が一つに減ります。
⑦ 組ごとに前に & を付けると ?&q=... のように最初の組の前にも & ができ、残った条件がないのに ?
を付けても間違いです。
⑧ 値のない行を 名前= として付けてはいけません。

Pythonmain.py · 13 行
from urllib.parse import quote base, n = input().split()pairs = []for _ in range(int(n)):    # 처음 나오는 빈칸에서만 자른다. 빈칸이 없으면 value는 빈 문자열이다    name, _, value = input().partition(" ")    if value == "":        continue    # quote는 기본으로 "/"를 그대로 둔다. safe=""로 그것도 바꾸게 한다    pairs.append(name + "=" + quote(value, safe=""))# 조각을 목록에 모았다가 join으로 한 번에 잇는다: 사이에만 "&"가 들어간다print(base + ("?" + "&".join(pairs) if pairs else ""))
JavaScriptmain.js · 19 行
const lines = require("fs").readFileSync(0, "utf8").split("\n");const [base, n] = lines[0].split(" "); // encodeURIComponent는 ! ' ( ) * 다섯 글자를 그대로 둔다. 그 다섯 글자는 따로 바꾼다function encode(value) {  return encodeURIComponent(value).replace(    /[!'()*]/g,    (c) => "%" + c.charCodeAt(0).toString(16).toUpperCase()  );} const pairs = [];for (let i = 1; i <= Number(n); i++) {  const line = lines[i];  const at = line.indexOf(" "); // split(" ", 2)는 셋째 조각부터 버리므로 쓰지 않는다  if (at === -1) continue;  pairs.push(line.slice(0, at) + "=" + encode(line.slice(at + 1)));}console.log(base + (pairs.length > 0 ? "?" + pairs.join("&") : ""));
TypeScriptmain.ts · 20 行
import { readFileSync } from "node:fs";const lines: string[] = readFileSync(0, "utf8").split("\n");const [base, n] = lines[0].split(" "); // encodeURIComponent는 ! ' ( ) * 다섯 글자를 그대로 둔다. 그 다섯 글자는 따로 바꾼다function encode(value: string): string {  return encodeURIComponent(value).replace(    /[!'()*]/g,    (c: string) => "%" + c.charCodeAt(0).toString(16).toUpperCase()  );} const pairs: string[] = [];for (let i = 1; i <= Number(n); i++) {  const line = lines[i];  const at = line.indexOf(" "); // split(" ", 2)는 셋째 조각부터 버리므로 쓰지 않는다  if (at === -1) continue;  pairs.push(line.slice(0, at) + "=" + encode(line.slice(at + 1)));}console.log(base + (pairs.length > 0 ? "?" + pairs.join("&") : ""));
JavaMain.java · 36 行
import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader; public class Main {    // URLEncoder.encode는 빈칸을 +로 바꾸고 ~를 바꿔 버려서 규칙과 다르다. 그래서 직접 만든다    static void appendEncoded(StringBuilder out, String value) {        for (int i = 0; i < value.length(); i++) {            char c = value.charAt(i);            boolean keep = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')                    || c == '-' || c == '_' || c == '.' || c == '~';            if (keep) {                out.append(c);            } else {                out.append(String.format("%%%02X", (int) c)); // %% 는 % 한 글자, %02X는 대문자 16진수 두 자리            }        }    }     public static void main(String[] args) throws IOException {        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));        String[] first = in.readLine().split(" ");        int n = Integer.parseInt(first[1]);        StringBuilder out = new StringBuilder(first[0]);        boolean any = false;        for (int i = 0; i < n; i++) {            String[] parts = in.readLine().split(" ", 2); // 많아야 두 조각: 둘째 조각에 나머지가 모두 남는다            if (parts.length < 2) continue;            out.append(any ? '&' : '?'); // 첫 쌍 앞에는 ?, 그다음부터는 &            any = true;            out.append(parts[0]).append('=');            appendEncoded(out, parts[1]);        }        System.out.println(out);    }}
C#Main.cs · 20 行
using System;using System.Collections.Generic; class Program{    static void Main()    {        string[] first = Console.ReadLine().Split(' ');        int n = int.Parse(first[1]);        var pairs = new List<string>();        for (int i = 0; i < n; i++)        {            string[] parts = Console.ReadLine().Split(' ', 2); // 많아야 두 조각: 둘째 조각에 나머지가 모두 남는다            if (parts.Length < 2) continue;            // Uri.EscapeDataString은 영문자·숫자와 - _ . ~ 만 남기고 %XX(대문자)로 바꾼다            pairs.Add(parts[0] + "=" + Uri.EscapeDataString(parts[1]));        }        Console.WriteLine(first[0] + (pairs.Count > 0 ? "?" + string.Join("&", pairs) : ""));    }}
C++main.cpp · 36 行
#include <iostream>#include <string> int main() {    std::string base;    int n;    std::cin >> base >> n;    std::string line;    std::getline(std::cin, line);  // 첫 줄의 나머지(줄바꿈)를 넘긴다     const std::string hex = "0123456789ABCDEF";    std::string out = base;  // std::string은 += 로 뒤에 이어 붙여도 빠르다    bool any = false;    for (int i = 0; i < n; i++) {        std::getline(std::cin, line);        auto at = line.find(' ');  // 처음 나오는 빈칸        if (at == std::string::npos) continue;        out += any ? '&' : '?';        any = true;        out += line.substr(0, at);        out += '=';        for (std::size_t k = at + 1; k < line.size(); k++) {            char c = line[k];            bool keep = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') ||                        c == '-' || c == '_' || c == '.' || c == '~';            if (keep) {                out += c;            } else {                out += '%';                out += hex[c / 16];  // 16진수 윗자리                out += hex[c % 16];  // 16진수 아랫자리            }        }    }    std::cout << out << '\n';}
Cmain.c · 30 行
#include <ctype.h>#include <stdio.h>#include <string.h> int main(void) {    char base[64];    int n;    scanf("%63s %d", base, &n);    getchar(); /* 첫 줄 끝의 줄바꿈을 넘긴다 */    fputs(base, stdout);    int any = 0;    char line[160];    for (int i = 0; i < n; i++) {        if (!fgets(line, sizeof line, stdin)) break;        line[strcspn(line, "\n")] = '\0';        char *space = strchr(line, ' '); /* 처음 나오는 빈칸 */        if (space == NULL) continue;        *space = '\0'; /* 그 자리에서 이름과 값을 끊는다 */        /* C에서는 문자열을 이어 붙이는 대신 조각을 바로바로 출력한다 */        printf("%c%s=", any ? '&' : '?', line);        any = 1;        for (const char *p = space + 1; *p != '\0'; p++) {            unsigned char c = (unsigned char)*p;            if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') putchar(c);            else printf("%%%02X", (unsigned)c); /* %X는 대문자, %x는 소문자 16진수 */        }    }    putchar('\n');    return 0;}
Craykonmain.cray · 45 行
십육진수 = "0123456789ABCDEF"; // 값의 글자를 하나씩 보고, 그대로 둘 글자가 아니면 %와 16진수 두 자리로 바꾼다바꾸기값(값){    결과 = "";    それぞれ (c の中 값)    {        그대로 = c >= "A" && c <= "Z" || c >= "a" && c <= "z" || c >= "0" && c <= "9" || c == "-" || c == "_" || c == "." || c == "~";        もし (그대로)        {            결과 = 결과 + c;        }        でなければ        {            코드 = 文字.コード(c);            결과 = 결과 + "%" + 십육진수[数学.商(코드, 16)] + 십육진수[코드 % 16];        }    }    返す 결과;} 첫줄 = コンソール.行読み().分ける(" ");n = 첫줄[1].数に();쌍들 = [];繰り返す (i = 0; i < n; i++){    줄 = コンソール.行読み();    자리 = 줄.位置検索(" "); // 처음 나오는 빈칸, 없으면 -1    もし (자리 == -1)    {        次へ;    }    이름 = 줄.切り出す(0, 자리);    값 = 줄.切り出す(자리 + 1, 줄.長さ - 자리 - 1);    쌍들.追加(이름 + "=" + 바꾸기값(값));}もし (쌍들.個数 > 0){    コンソール.書く(첫줄[0] + "?" + 쌍들.つなぐ("&"));}でなければ{    コンソール.書く(첫줄[0]);}