画面にこう出れば正解
どの言語で書いても、同じ入力なら画面に出る文字は1文字も違わない、というのが正解の決め方です(例は Python で動かしたときのコマンドです)。
基本
input.txt の中身
名前,点数 山田,80 佐藤,72
$ python3 scores.py < input.txt
山田: 80
佐藤: 72
合計: 152(2人)名前にカンマ("…" で囲む)
input.txt の中身
名前,点数 "山田, 太郎",80 佐藤,72
$ python3 scores.py < input.txt
山田, 太郎: 80
佐藤: 72
合計: 152(2人)名前に " ("" と2つ重ねる)
input.txt の中身
名前,点数 "自称""先生""",90 山田,80
$ python3 scores.py < input.txt
自称"先生": 90
山田: 80
合計: 170(2人)改行が CRLF(Windows・Excel)
input.txt の中身
名前,点数[CR] 山田,80[CR] 佐藤,72[CR]
$ python3 scores.py < input.txt
山田: 80
佐藤: 72
合計: 152(2人)先頭に BOM(Excel の「CSV UTF-8」)
input.txt の中身
[BOM]名前,点数 山田,80 佐藤,72
$ python3 scores.py < input.txt
山田: 80
佐藤: 72
合計: 152(2人)最後に改行が無い
input.txt の中身
名前,点数 山田,80 佐藤,72
$ python3 scores.py < input.txt
山田: 80
佐藤: 72
合計: 152(2人)空の行がある
input.txt の中身
名前,点数 山田,80 佐藤,72
$ python3 scores.py < input.txt
山田: 80
佐藤: 72
合計: 152(2人)見出しだけ
input.txt の中身
名前,点数
$ python3 scores.py < input.txt
データがありません見出しが無い
input.txt の中身
山田,80 佐藤,72
$ python3 scores.py < input.txt
見出しの行がありません10言語のプログラム
どれもそのまま保存して動かせます。毎朝、上の正解の例をすべて実際に動かして確かめています。
scores.php8.4.26 で 9 / 9 件の出力が正解と一致
<?php
// 標準入力の CSV(名前,点数)を読んで、1人ずつの点数と合計を出す
$text = stream_get_contents(STDIN);
// 先頭の BOM(EF BB BF の3バイト)を取り除く
if (str_starts_with($text, "\u{FEFF}")) {
$text = substr($text, 3);
}
$rows = [];
// 改行は LF でも CRLF でもよいので、\r\n と \n のどちらでも区切る。空の行は飛ばす
foreach (preg_split('/\r?\n/', $text) as $line) {
if ($line === '') {
continue;
}
// str_getcsv は "…" で囲んだ項目や "" を読み分ける。PHP 8.4 から escape(4つ目)を書かないと警告が出るので "" と書く
$rows[] = str_getcsv($line, ',', '"', '');
}
if (count($rows) === 0 || $rows[0] !== ['名前', '点数']) {
echo "見出しの行がありません\n";
exit;
}
if (count($rows) === 1) {
echo "データがありません\n";
exit;
}
$total = 0;
foreach (array_slice($rows, 1) as [$name, $score]) {
echo $name . ": " . (int) $score . "\n";
$total += (int) $score;
}
echo "合計: " . $total . "(" . (count($rows) - 1) . "人)\n";
動かし方(ファイルを保存したフォルダで) ・ PHP を入れる
$ php scores.php < input.txt
scores.py3.14.7 で 9 / 9 件の出力が正解と一致
# 標準入力の CSV(名前,点数)を読んで、1人ずつの点数と合計を出す
import csv
import sys
# utf-8-sig は先頭の BOM を取り除く。newline="" で改行の扱いを csv に任せる(CRLF も "…" の中の改行も読める)
sys.stdin.reconfigure(encoding="utf-8-sig", newline="")
# csv は "…" で囲んだ項目や "" を読み分ける。空の行は [] になるので飛ばす
rows = [row for row in csv.reader(sys.stdin) if row]
if not rows or rows[0] != ["名前", "点数"]:
print("見出しの行がありません")
sys.exit()
if len(rows) == 1:
print("データがありません")
sys.exit()
total = 0
for name, score in rows[1:]:
print(f"{name}: {int(score)}")
total += int(score)
print(f"合計: {total}({len(rows) - 1}人)")
動かし方(ファイルを保存したフォルダで) ・ Python を入れる
$ python3 scores.py < input.txt
scores.go1.27.2 で 9 / 9 件の出力が正解と一致
// 標準入力の CSV(名前,点数)を読んで、1人ずつの点数と合計を出す
package main
import (
"encoding/csv"
"fmt"
"io"
"os"
"strconv"
"strings"
)
func main() {
data, _ := io.ReadAll(os.Stdin)
// encoding/csv は BOM を取り除かないので、先頭の U+FEFF を自分で取る
text := strings.TrimPrefix(string(data), "\uFEFF")
// encoding/csv は "…" で囲んだ項目・""・CRLF を読み分け、空の行も飛ばす
rows, err := csv.NewReader(strings.NewReader(text)).ReadAll()
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
if len(rows) == 0 || len(rows[0]) != 2 || rows[0][0] != "名前" || rows[0][1] != "点数" {
fmt.Println("見出しの行がありません")
return
}
if len(rows) == 1 {
fmt.Println("データがありません")
return
}
total := 0
for _, row := range rows[1:] {
score, err := strconv.Atoi(row[1])
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
fmt.Printf("%s: %d\n", row[0], score)
total += score
}
fmt.Printf("合計: %d(%d人)\n", total, len(rows)-1)
}
動かし方(ファイルを保存したフォルダで) ・ Go を入れる
$ go run scores.go < input.txt
scores.js24.15.0 で 9 / 9 件の出力が正解と一致
// 標準入力の CSV(名前,点数)を読んで、1人ずつの点数と合計を出す
// JavaScript には CSV を読む標準の機能が無いので、1文字ずつ見て「今 "…" の中にいるか」を覚えながら区切る
function parseCsv(text) {
const rows = [];
let row = [];
let field = "";
let inQuotes = false;
for (let i = 0; i < text.length; i++) {
const c = text[i];
if (inQuotes) {
if (c === '"' && text[i + 1] === '"') {
field += '"'; // "" は " 1つ
i++;
} else if (c === '"') {
inQuotes = false;
} else {
field += c; // "…" の中のカンマや改行は、そのまま項目の一部
}
} else if (c === '"') {
inQuotes = true;
} else if (c === ",") {
row.push(field);
field = "";
} else if (c === "\n") {
row.push(field);
rows.push(row);
row = [];
field = "";
} else if (c !== "\r") { // CRLF の CR は読み飛ばす
field += c;
}
}
if (field !== "" || row.length > 0) { // 最後に改行が無いときの最後の行
row.push(field);
rows.push(row);
}
return rows;
}
// 先頭の BOM(U+FEFF)を取り除く。空の行は [""] になるので飛ばす
const text = require("fs").readFileSync(0, "utf8").replace(/^\uFEFF/, "");
const rows = parseCsv(text).filter((row) => !(row.length === 1 && row[0] === ""));
if (rows.length === 0 || rows[0].length !== 2 || rows[0][0] !== "名前" || rows[0][1] !== "点数") {
console.log("見出しの行がありません");
process.exit();
}
if (rows.length === 1) {
console.log("データがありません");
process.exit();
}
let total = 0;
for (const [name, score] of rows.slice(1)) {
console.log(`${name}: ${Number(score)}`);
total += Number(score);
}
console.log(`合計: ${total}(${rows.length - 1}人)`);
動かし方(ファイルを保存したフォルダで) ・ JavaScript を入れる
$ node scores.js < input.txt
Scores.java21.0.12 で 9 / 9 件の出力が正解と一致
// 標準入力の CSV(名前,点数)を読んで、1人ずつの点数と合計を出す
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
public class Scores {
// 1行を項目に分ける。「"」の中にいる間のカンマは区切りではなく、名前の一部として扱う
static List<String> splitLine(String line) {
List<String> cols = new ArrayList<>();
StringBuilder cur = new StringBuilder();
boolean inQuotes = false;
for (int i = 0; i < line.length(); i++) {
char c = line.charAt(i);
if (inQuotes) {
if (c == '"' && i + 1 < line.length() && line.charAt(i + 1) == '"') {
cur.append('"'); // 「""」は「"」1つ
i++;
} else if (c == '"') {
inQuotes = false;
} else {
cur.append(c);
}
} else if (c == '"') {
inQuotes = true;
} else if (c == ',') {
cols.add(cur.toString());
cur.setLength(0);
} else {
cur.append(c);
}
}
cols.add(cur.toString());
return cols;
}
public static void main(String[] args) throws IOException {
// readLine は \n でも \r\n でも行を区切り、行の終わりの記号を取って返す
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
String header = in.readLine();
// Excel の「CSV UTF-8」は先頭に BOM(U+FEFF)が付くので、取ってから比べる
if (header != null && header.startsWith("\uFEFF")) {
header = header.substring(1);
}
if (header == null || !splitLine(header).equals(List.of("名前", "点数"))) {
System.out.println("見出しの行がありません");
return;
}
int total = 0;
int count = 0;
String line;
while ((line = in.readLine()) != null) {
if (line.isEmpty()) {
continue;
}
List<String> cols = splitLine(line);
int score = Integer.parseInt(cols.get(1));
System.out.println(cols.get(0) + ": " + score);
total += score;
count++;
}
if (count == 0) {
System.out.println("データがありません");
} else {
System.out.println("合計: " + total + "(" + count + "人)");
}
}
}
動かし方(ファイルを保存したフォルダで) ・ Java を入れる
$ java Scores.java < input.txt
scores.cs10.0.402 で 9 / 9 件の出力が正解と一致
// 標準入力の CSV(名前,点数)を読んで、1人ずつの点数と合計を出す
using System.Text;
// Console.ReadLine は \n でも \r\n でも行を区切り、行の終わりの記号を取って返す
string? header = Console.ReadLine();
// Excel の「CSV UTF-8」は先頭に BOM(U+FEFF)が付くので、取ってから比べる
if (header != null && header.StartsWith('\uFEFF'))
{
header = header.Substring(1);
}
if (header == null || !SplitLine(header).SequenceEqual(new[] { "名前", "点数" }))
{
Console.WriteLine("見出しの行がありません");
return;
}
int total = 0;
int count = 0;
string? line;
while ((line = Console.ReadLine()) != null)
{
if (line == "")
{
continue;
}
List<string> cols = SplitLine(line);
int score = int.Parse(cols[1]);
Console.WriteLine($"{cols[0]}: {score}");
total += score;
count++;
}
if (count == 0)
{
Console.WriteLine("データがありません");
}
else
{
Console.WriteLine($"合計: {total}({count}人)");
}
// 1行を項目に分ける。「"」の中にいる間のカンマは区切りではなく、名前の一部として扱う
static List<string> SplitLine(string line)
{
var cols = new List<string>();
var cur = new StringBuilder();
bool inQuotes = false;
for (int i = 0; i < line.Length; i++)
{
char c = line[i];
if (inQuotes)
{
if (c == '"' && i + 1 < line.Length && line[i + 1] == '"')
{
cur.Append('"'); // 「""」は「"」1つ
i++;
}
else if (c == '"')
{
inQuotes = false;
}
else
{
cur.Append(c);
}
}
else if (c == '"')
{
inQuotes = true;
}
else if (c == ',')
{
cols.Add(cur.ToString());
cur.Clear();
}
else
{
cur.Append(c);
}
}
cols.Add(cur.ToString());
return cols;
}
動かし方(ファイルを保存したフォルダで) ・ C# を入れる
$ dotnet run scores.cs -- < input.txt
scores.rs1.99.0 で 9 / 9 件の出力が正解と一致
// 標準入力の CSV(名前,点数)を読んで、1人ずつの点数と合計を出す
use std::io::BufRead;
// 1行を項目に分ける。「"」の中にいる間のカンマは区切りではなく、名前の一部として扱う
fn split_line(line: &str) -> Vec<String> {
let mut cols = Vec::new();
let mut cur = String::new();
let mut in_quotes = false;
let mut chars = line.chars().peekable();
while let Some(c) = chars.next() {
if in_quotes {
if c == '"' && chars.peek() == Some(&'"') {
cur.push('"'); // 「""」は「"」1つ
chars.next();
} else if c == '"' {
in_quotes = false;
} else {
cur.push(c);
}
} else if c == '"' {
in_quotes = true;
} else if c == ',' {
cols.push(cur.clone());
cur.clear();
} else {
cur.push(c);
}
}
cols.push(cur);
cols
}
fn main() {
// lines() は \n でも \r\n でも行を区切り、行の終わりの記号を取って返す
let stdin = std::io::stdin();
let mut lines = stdin.lock().lines();
let header = match lines.next() {
Some(h) => h.unwrap(),
None => String::new(),
};
// Excel の「CSV UTF-8」は先頭に BOM(\u{FEFF})が付くので、取ってから比べる
let header = header.strip_prefix('\u{FEFF}').unwrap_or(&header);
if split_line(header) != ["名前", "点数"] {
println!("見出しの行がありません");
return;
}
let mut total = 0;
let mut count = 0;
for line in lines {
let line = line.unwrap();
if line.is_empty() {
continue;
}
let cols = split_line(&line);
let score: i32 = cols[1].parse().unwrap();
println!("{}: {}", cols[0], score);
total += score;
count += 1;
}
if count == 0 {
println!("データがありません");
} else {
println!("合計: {}({}人)", total, count);
}
}
動かし方(ファイルを保存したフォルダで) ・ Rust を入れる
$ rustc scores.rs && ./scores < input.txt
scores.rb4.0.7 で 9 / 9 件の出力が正解と一致
# 標準入力の CSV(名前,点数)を読んで、1人ずつの点数と合計を出す
require "csv" # Ruby 3.4 から「同梱の gem」になったが、Ruby と一緒に入っている
# 文字コードを UTF-8 と決めて読み(環境によって決まらないことがある)、先頭の BOM を取り除く
text = $stdin.read.force_encoding("UTF-8").delete_prefix("\uFEFF")
# CSV は "…" で囲んだ項目・""・CRLF を読み分ける。空の行は [] になるので飛ばす
rows = CSV.parse(text).reject(&:empty?)
if rows.empty? || rows[0] != ["名前", "点数"]
puts "見出しの行がありません"
exit
end
if rows.length == 1
puts "データがありません"
exit
end
total = 0
rows[1..].each do |name, score|
puts "#{name}: #{Integer(score, 10)}"
total += Integer(score, 10)
end
puts "合計: #{total}(#{rows.length - 1}人)"
動かし方(ファイルを保存したフォルダで) ・ Ruby を入れる
$ ruby scores.rb < input.txt
scores.kt2.4.21 で 9 / 9 件の出力が正解と一致
// 標準入力の CSV(名前,点数)を読んで、1人ずつの点数と合計を出す
// 1行を項目に分ける。「"」の中にいる間のカンマは区切りではなく、名前の一部として扱う
fun splitLine(line: String): List<String> {
val cols = mutableListOf<String>()
val cur = StringBuilder()
var inQuotes = false
var i = 0
while (i < line.length) {
val c = line[i]
if (inQuotes) {
if (c == '"' && i + 1 < line.length && line[i + 1] == '"') {
cur.append('"') // 「""」は「"」1つ
i++
} else if (c == '"') {
inQuotes = false
} else {
cur.append(c)
}
} else if (c == '"') {
inQuotes = true
} else if (c == ',') {
cols.add(cur.toString())
cur.clear()
} else {
cur.append(c)
}
i++
}
cols.add(cur.toString())
return cols
}
fun main() {
// readLine は \n でも \r\n でも行を区切り、行の終わりの記号を取って返す
// Excel の「CSV UTF-8」は先頭に BOM(U+FEFF)が付くので、取ってから比べる
val header = readLine()?.removePrefix("\uFEFF")
if (header == null || splitLine(header) != listOf("名前", "点数")) {
println("見出しの行がありません")
return
}
var total = 0
var count = 0
while (true) {
val line = readLine() ?: break
if (line.isEmpty()) continue
val cols = splitLine(line)
val score = cols[1].toInt()
println("${cols[0]}: $score")
total += score
count++
}
if (count == 0) {
println("データがありません")
} else {
println("合計: $total(${count}人)")
}
}
動かし方(ファイルを保存したフォルダで) ・ Kotlin を入れる
$ kotlinc scores.kt -include-runtime -d scores.jar && java -jar scores.jar < input.txt
scores.swift6.3.3 で 9 / 9 件の出力が正解と一致
// 標準入力の CSV(名前,点数)を読んで、1人ずつの点数と合計を出す
// 1行を項目に分ける。「"」の中にいる間のカンマは区切りではなく、名前の一部として扱う
func splitLine(_ line: String) -> [String] {
var cols: [String] = []
var cur = ""
var inQuotes = false
let chars = Array(line)
var i = 0
while i < chars.count {
let c = chars[i]
if inQuotes {
if c == "\"" && i + 1 < chars.count && chars[i + 1] == "\"" {
cur.append("\"") // 「""」は「"」1つ
i += 1
} else if c == "\"" {
inQuotes = false
} else {
cur.append(c)
}
} else if c == "\"" {
inQuotes = true
} else if c == "," {
cols.append(cur)
cur = ""
} else {
cur.append(c)
}
i += 1
}
cols.append(cur)
return cols
}
// readLine は \n でも \r\n でも行を区切り、行の終わりの記号を取って返す
var header = readLine()
// Excel の「CSV UTF-8」は先頭に BOM(\u{FEFF})が付くので、取ってから比べる
if let h = header, h.hasPrefix("\u{FEFF}") {
header = String(h.dropFirst())
}
if header == nil || splitLine(header!) != ["名前", "点数"] {
print("見出しの行がありません")
} else {
var total = 0
var count = 0
while let line = readLine() {
if line.isEmpty {
continue
}
let cols = splitLine(line)
let score = Int(cols[1])!
print("\(cols[0]): \(score)")
total += score
count += 1
}
if count == 0 {
print("データがありません")
} else {
print("合計: \(total)(\(count)人)")
}
}
動かし方(ファイルを保存したフォルダで) ・ Swift を入れる
$ swift scores.swift < input.txt
言語でつまずきやすいところ
初心者がよく書く形(各言語で最初に習う書き方をそのまま使ったもの)を、同じ正解表で動かした結果です。言語を出した答えごとにまとめ、赤い字はその言語が実際に出した間違った行です。
| 入力 | 初心者がよく書く形の答え(10言語) |
|---|---|
| 基本 | ✓10言語すべて正解 |
| 名前にカンマ("…" で囲む) |
|
| 名前に " ("" と2つ重ねる) |
|
| 改行が CRLF(Windows・Excel) |
|
| 先頭に BOM(Excel の「CSV UTF-8」) |
|
| 最後に改行が無い | ✓10言語すべて正解 |
| 空の行がある | ✓10言語すべて正解 |
| 見出しだけ | ✓10言語すべて正解 |
| 見出しが無い | ✓10言語すべて正解 |
この回でやること
- 標準入力から、1行ずつ読む
- CSV の1行を、項目に分ける
- 数に直して、合計する
この回の決まり
- 1行目は見出しの「名前,点数」。無ければ「見出しの行がありません」、見出しだけなら「データがありません」
- 項目にカンマや
"を含むときは、"…"で囲む。中の"は""と2つ重ねる(CSV の決まり。RFC 4180) - 改行は LF でも CRLF でもよい。ファイルの先頭の BOM(Excel の「CSV UTF-8」で保存すると付く印)は無視する。空の行は飛ばす
解説
「,」で区切るだけでは読めない CSV
山田,80 は「,」で区切れば読めます。ところが、名前にカンマが入ると "山田, 太郎",80 のように " で囲んで書くのが CSV の決まりです。これを「,」で区切ると、"山田 と 太郎" と 80 の3つに分かれてしまいます。
「,」で区切るだけのプログラムに "山田, 太郎",80 を渡すと、項目が3つに分かれます。Python・Java・Kotlin・C#・Rust・Swift はエラーで止まりました。Go・PHP・Ruby は "山田: 0、JavaScript は "山田: NaN と、間違った答えを黙って出しました。"自称""先生""" は、10言語とも " が残ったまま出ました。
標準で CSV を読む機能を持つ言語(Python の csv・Go の encoding/csv・Ruby の CSV・PHP の str_getcsv など)は、それを使います。持たない言語では、1文字ずつ見て「今 " の中にいるか」を覚えながら区切ります。
Windows・Excel で作った CSV
Excel で保存した CSV は、改行が CRLF(\r\n)で、先頭に BOM(目に見えない印 U+FEFF)が付くことがあります。
- CRLF: 全部を読んでから
\nで区切ると、行の最後に\rが残ります。Python・JavaScript・PHP・Ruby をこの書き方にしたところ、見出しの行が「名前,点数\r」になって合わず、「見出しの行がありません」と出ました。Go(bufio.Scanner)・Java・Kotlin・C#(ReadLine)・Rust(lines())・Swift(readLine)の、1行ずつ読む機能は\rも取るので通りました - BOM: 10言語とも、行を読む機能は BOM を取りませんでした。見出しが「[BOM]名前,点数」になり、全言語で「見出しの行がありません」と出ました。自分で取り除きます
例の表示について
下の例では、目に見えない BOM を [BOM]、CRLF の CR を [CR] と書いています。
練習
- 平均点も出してみましょう(小数第1位まで)
- 点数が数でない行があったら、その行番号を出して止めるようにしてみましょう