画面にこう出れば正解
どの言語で書いても、同じ入力なら画面に出る文字は1文字も違わない、というのが正解の決め方です(例は Python で動かしたときのコマンドです)。
基本
$ python3 truncate.py こんにちは世界 5
こんにちは…ちょうど
$ python3 truncate.py こんにちは 5
こんにちは短い
$ python3 truncate.py abc 5
abc英数字
$ python3 truncate.py "Hello, World" 5
Hello…絵文字
$ python3 truncate.py 🍣🍺🍜🍙 2
🍣🍺…めずらしい漢字
$ python3 truncate.py 𠮷野家の牛丼 2
𠮷野…絵文字の手前で切る
$ python3 truncate.py 今日は🍣 3
今日は…0文字
$ python3 truncate.py こんにちは 0
…10言語のプログラム
どれもそのまま保存して動かせます。毎朝、上の正解の例をすべて実際に動かして確かめています。
truncate.php8.4.26 で 8 / 8 件の出力が正解と一致
<?php
// 文字列の先頭から決まった文字数だけを出し、残りがあれば「…」を付ける
$s = $argv[1];
$n = (int) $argv[2];
// strlen・substr はバイトで数えるので、文字の途中で切れる。/u を付けた preg_split で1文字ずつの配列に直す
// (mb_substr は mbstring 拡張が無いと使えない)
$chars = preg_split('//u', $s, -1, PREG_SPLIT_NO_EMPTY);
if (count($chars) > $n) {
echo implode('', array_slice($chars, 0, $n)) . "…\n";
} else {
echo $s . "\n";
}
動かし方(ファイルを保存したフォルダで) ・ PHP を入れる
$ php truncate.php こんにちは世界 5
truncate.py3.14.7 で 8 / 8 件の出力が正解と一致
# 文字列の先頭から決まった文字数だけを出し、残りがあれば「…」を付ける
import sys
s = sys.argv[1]
n = int(sys.argv[2])
# Python の文字列は、len もスライスも Unicode の文字(コードポイント)で数えるので、「🍣」も「𠮷」も1文字
if len(s) > n:
print(s[:n] + "…")
else:
print(s)
動かし方(ファイルを保存したフォルダで) ・ Python を入れる
$ python3 truncate.py こんにちは世界 5
truncate.go1.27.2 で 8 / 8 件の出力が正解と一致
// 文字列の先頭から決まった文字数だけを出し、残りがあれば「…」を付ける
package main
import (
"fmt"
"os"
"strconv"
)
func main() {
s := os.Args[1]
n, _ := strconv.Atoi(os.Args[2])
// len(s) と s[:n] はバイトで数えるので、文字の途中で切れる。[]rune に直すと Unicode の文字1つずつの並びになる
chars := []rune(s)
if len(chars) > n {
fmt.Println(string(chars[:n]) + "…")
} else {
fmt.Println(s)
}
}
動かし方(ファイルを保存したフォルダで) ・ Go を入れる
$ go run truncate.go こんにちは世界 5
truncate.js24.15.0 で 8 / 8 件の出力が正解と一致
// 文字列の先頭から決まった文字数だけを出し、残りがあれば「…」を付ける
const s = process.argv[2];
const n = Number(process.argv[3]);
// length と slice は UTF-16 の単位で数えるので、「🍣」や「𠮷」は2つと数える。[...s] で Unicode の文字1つずつの配列に直す
const chars = [...s];
if (chars.length > n) {
console.log(chars.slice(0, n).join("") + "…");
} else {
console.log(s);
}
動かし方(ファイルを保存したフォルダで) ・ JavaScript を入れる
$ node truncate.js こんにちは世界 5
Truncate.java21.0.12 で 8 / 8 件の出力が正解と一致
// 受け取った文字列の先頭から、決まった文字数だけを出す。残りがあれば「…」を付ける
public class Truncate {
public static void main(String[] args) {
String s = args[0];
int n = Integer.parseInt(args[1]);
// length と substring は UTF-16 の単位で数えるので、「🍣」や「𠮷」は2つになる。
// codePoints で Unicode の文字(コードポイント)の並びに直してから数え、切る
int[] chars = s.codePoints().toArray();
if (chars.length > n) {
System.out.println(new String(chars, 0, n) + "…");
} else {
System.out.println(s);
}
}
}
動かし方(ファイルを保存したフォルダで) ・ Java を入れる
$ java Truncate.java こんにちは世界 5
truncate.cs10.0.402 で 8 / 8 件の出力が正解と一致
// 受け取った文字列の先頭から、決まった文字数だけを出す。残りがあれば「…」を付ける
string s = args[0];
int n = int.Parse(args[1]);
// Length と Substring は UTF-16 の単位で数えるので、「🍣」や「𠮷」は2つになる。
// EnumerateRunes で Unicode の文字(コードポイント)の並びに直してから数え、切る
List<System.Text.Rune> chars = s.EnumerateRunes().ToList();
if (chars.Count > n)
{
Console.WriteLine(string.Concat(chars.Take(n)) + "…");
}
else
{
Console.WriteLine(s);
}
動かし方(ファイルを保存したフォルダで) ・ C# を入れる
$ dotnet run truncate.cs -- こんにちは世界 5
truncate.rs1.99.0 で 8 / 8 件の出力が正解と一致
// 受け取った文字列の先頭から、決まった文字数だけを出す。残りがあれば「…」を付ける
fn main() {
let args: Vec<String> = std::env::args().collect();
let s = &args[1];
let n: usize = args[2].parse().unwrap();
// len と &s[..n] はバイトの単位なので、日本語の途中で切ろうとすると止まる。
// chars で Unicode の文字(コードポイント)の単位にしてから数え、切る
if s.chars().count() > n {
let head: String = s.chars().take(n).collect();
println!("{}…", head);
} else {
println!("{}", s);
}
}
動かし方(ファイルを保存したフォルダで) ・ Rust を入れる
$ rustc truncate.rs && ./truncate こんにちは世界 5
truncate.rb4.0.7 で 8 / 8 件の出力が正解と一致
# 文字列の先頭から決まった文字数だけを出し、残りがあれば「…」を付ける s = ARGV[0] n = ARGV[1].to_i # Ruby の文字列(UTF-8)は、length も s[0, n] も Unicode の文字(コードポイント)で数えるので、「🍣」も「𠮷」も1文字 if s.length > n puts s[0, n] + "…" else puts s end
動かし方(ファイルを保存したフォルダで) ・ Ruby を入れる
$ ruby truncate.rb こんにちは世界 5
truncate.kt2.4.21 で 8 / 8 件の出力が正解と一致
// 受け取った文字列の先頭から、決まった文字数だけを出す。残りがあれば「…」を付ける
fun main(args: Array<String>) {
val s = args[0]
val n = args[1].toInt()
// length と substring は UTF-16 の単位で数えるので、「🍣」や「𠮷」は2つになる。
// codePoints で Unicode の文字(コードポイント)の並びに直してから数え、切る
val chars = s.codePoints().toArray()
if (chars.size > n) {
println(String(chars, 0, n) + "…")
} else {
println(s)
}
}
動かし方(ファイルを保存したフォルダで) ・ Kotlin を入れる
$ kotlinc truncate.kt -include-runtime -d truncate.jar && java -jar truncate.jar こんにちは世界 5
truncate.swift6.3.3 で 8 / 8 件の出力が正解と一致
// 受け取った文字列の先頭から、決まった文字数だけを出す。残りがあれば「…」を付ける
let s = CommandLine.arguments[1]
let n = Int(CommandLine.arguments[2])!
// count と prefix は見た目の1文字(書記素クラスタ)で数える。この回の決まりに合わせて、
// unicodeScalars で Unicode の文字(コードポイント)の単位にしてから数え、切る
let chars = s.unicodeScalars
if chars.count > n {
print(String(String.UnicodeScalarView(chars.prefix(n))) + "…")
} else {
print(s)
}
動かし方(ファイルを保存したフォルダで) ・ Swift を入れる
$ swift truncate.swift こんにちは世界 5
言語でつまずきやすいところ
初心者がよく書く形(各言語で最初に習う書き方をそのまま使ったもの)を、同じ正解表で動かした結果です。言語を出した答えごとにまとめ、赤い字はその言語が実際に出した間違った行です。
| 入力 | 初心者がよく書く形の答え(10言語) |
|---|---|
基本こんにちは世界 5 |
|
ちょうどこんにちは 5 |
|
短いabc 5 | ✓10言語すべて正解 |
英数字"Hello, World" 5 | ✓10言語すべて正解 |
絵文字🍣🍺🍜🍙 2 |
|
めずらしい漢字𠮷野家の牛丼 2 |
|
絵文字の手前で切る今日は🍣 3 |
|
0文字こんにちは 0 | ✓10言語すべて正解 |
この回でやること
- 文字列の先頭から、決まった文字数だけを取り出す
- 残りがあるときだけ「…」を付ける
この回の決まり
- 1文字は、Unicode の文字1つ(コードポイント)で数える。「🍣」も「𠮷」も1文字
- 文字数を超えたときだけ、最後に「…」を付ける。ちょうどなら付けない
解説
「文字数」の数え方は、言語によって違う
第4回 で見たとおり、「文字数」の数え方は言語によって違います。バイト数で数える言語、UTF-16 の単位で数える言語、Unicode の文字で数える言語があります。
先頭を切り出すときも同じです。各言語のいちばん普通の書き方で試した結果です。
| 書き方 | 数える単位 | 「こんにちは世界」の先頭5文字 | 「🍣🍺🍜🍙」の先頭2文字 |
|---|---|---|---|
| 正解 | 文字 | こんにちは… | 🍣🍺… |
Python スライス・Ruby [0, n]・Swift prefix | 文字 | こんにちは… | 🍣🍺… |
JavaScript slice・Java/Kotlin substring・C# Substring | UTF-16 | こんにちは… | 🍣…(1文字少ない) |
PHP substr・Go s[:n] | バイト | こ�…(文字の途中で切れて壊れる) | �… |
Rust &s[..n] | バイト | 止まる(文字の境目でないとエラー) | 止まる |
UTF-16 で数える言語では、絵文字や「𠮷」が2つ分に数えられます。この回の入力ではちょうど絵文字の切れ目で切れたので、1文字少なくなるだけで済みました。切る位置が奇数なら、絵文字の半分だけが残った壊れた文字列になります。
文字の単位で切る
文字列を、いったん「文字の並び」(配列)に直してから先頭を取るか、文字の単位で数える関数を使います。
なお、「👨👩👧」のように、いくつもの文字を組み合わせて1つに見える絵文字もあります。これを「1文字」として扱うには、さらに別の数え方(書記素クラスタ)が要ります。この回では扱いません。
練習
- 「…」を付けた結果が、必ず決まった文字数に収まるようにしてみましょう(「…」も1文字と数える)
- 文字数ではなく、画面の幅(全角は2、半角は1)で切ってみましょう