コンテンツへスキップ

文字列

前のレッスンでは、ほとんどのプログラムの主要な構成要素である文字列について既に触れました。基本的な特性を復習しましょう。

文字列は、Unicode文字の列であり、UTF-8でエンコードされています。文字列はイミュータブルです。文字列を変更すると、実際には変更された内容を持つ新しい文字列が生成されます。元の文字列はそのまま残ります。

文字列は、通常、二重引用符(")で囲まれたリテラルとして記述されます。

埋め込み

文字列の埋め込みは、文字列を結合するための便利な方法です。文字列リテラル内の#{...}は、中括弧の間の式の値を文字列のこの位置に挿入します。

name = "Crystal"
puts "Hello #{name}"

埋め込み内の式は、変数または単純なメソッド呼び出しのいずれかに短く保つ必要があります。複雑な式はコードの可読性を低下させます。

式の値は文字列である必要はありません。任意の型が使用でき、#to_sメソッドを呼び出すことによって文字列表現に変換されます。このメソッドは、任意のオブジェクトに対して定義されています。数値で試してみましょう。

name = 6
puts "Hello #{name}!"

注意

埋め込みの代替手段は連結です。"Hello #{name}!"の代わりに、"Hello " + name + "!"と記述できます。しかし、これはかさばり、文字列型以外の場合にはいくつかの落とし穴があります。一般的に、連結よりも埋め込みが推奨されます。

エスケープ

一部の文字は、文字列リテラルに直接記述することはできません。たとえば、二重引用符です。文字列内で使用すると、コンパイラはそれを終了デリミタとして解釈します。

この問題の解決策はエスケープです。二重引用符の前にバックスラッシュ(\)が付いている場合、それはエスケープシーケンスとして解釈され、両方の文字が組み合わさって二重引用符文字をエンコードします。

puts "I say: \"Hello World!\""

他にもエスケープシーケンスがあります。たとえば、改行(\n)やタブ(\t)などの印刷不可能な文字です。リテラルのバックスラッシュを記述する場合は、エスケープシーケンスは二重バックスラッシュ(\\)です。null文字(コードポイント0)は、Crystal文字列の通常の文字です。一部のプログラミング言語では、この文字は文字列の終端を示します。しかし、Crystalでは、その#sizeプロパティによってのみ決定されます。

puts "I say: \"Hello \\\n\tWorld!\""

ヒント

使用可能なエスケープシーケンスの詳細については、文字列リテラルリファレンスを参照してください。

代替デリミタ

一部の文字列リテラルには、多数の二重引用符が含まれている場合があります。たとえば、引用符で囲まれた引数値を持つHTMLタグを考えてみてください。それぞれをバックスラッシュでエスケープしなければならないのは面倒でしょう。代替リテラルデリミタは、便利な代替手段です。%(...)は、二重引用符の代わりに、デリミタが括弧(())で示されることを除いて、"..."と同等です。

puts %(I say: "Hello World!")

エスケープシーケンスと埋め込みは、これまでと同様に機能します。

ヒント

代替デリミタの詳細については、文字列リテラルリファレンスを参照してください。

Unicode

Unicodeは、さまざまな筆記体系でテキストを表すための国際規格です。英語や他の多くの言語で使用されているラテンアルファベットの文字に加えて、他の多くの文字セットも含まれています。プレーンテキストだけでなく、Unicode標準には絵文字やアイコンも含まれています。

次の例では、Unicode文字U+1F310子午線を持つ地球を使用して世界に対応します。

puts "Hello 🌐"

Unicode記号を扱うのは、時には少し難しい場合があります。一部の文字はエディターフォントでサポートされていない場合や、一部の文字は印刷できない場合もあります。代替手段として、Unicode文字はエスケープシーケンスとして表現できます。バックスラッシュの後に文字uが付くと、Unicodeコードポイントを示します。コードポイント値は、中括弧で囲まれた16進数として記述されます。コードポイントに正確に4桁がある場合は、中括弧を省略できます。

puts "Hello \u{1F310}"

変換

文字列について何かを変更したいとしましょう。メッセージを叫んで、すべてを大文字にしたいですか?String#upcaseメソッドは、すべての小文字を対応する大文字に変換します。反対はString#downcaseです。メッセージをさまざまなスタイルで表現できる、いくつかの同様のメソッドがあります。

message = "Hello World! Greetings from Crystal."

puts "normal: #{message}"
puts "upcased: #{message.upcase}"
puts "downcased: #{message.downcase}"
puts "camelcased: #{message.camelcase}"
puts "capitalized: #{message.capitalize}"
puts "reversed: #{message.reverse}"
puts "titleized: #{message.titleize}"
puts "underscored: #{message.underscore}"

#camelcaseメソッドと#underscoreメソッドは、この特定の文字列を変更しませんが、入力"snake_cased"または"CamelCased"で試してみてください。

情報

文字列をより詳細に見て、それについて何を知ることができるかを見てみましょう。まず、文字列には長さ、つまり含まれる文字数があります。この値はString#sizeとして使用できます。

message = "Hello World! Greetings from Crystal."

p! message.size

文字列が空かどうかを判断するには、サイズがゼロかどうかを確認するか、短縮形のString#empty?を使用します。

empty_string = ""

p! empty_string.size == 0,
  empty_string.empty?

文字列が空の場合、または空白文字のみを含む場合は、メソッドString#blank?trueを返します。関連するメソッドはString#presenceであり、文字列が空白の場合はnilを返し、それ以外の場合は文字列自体を返します。

blank_string = ""

p! blank_string.blank?,
  blank_string.presence

等価性と比較

文字列の等価性をテストするには、等価演算子(==)を使用し、比較するには比較演算子(<=>)を使用できます。どちらも文字列を厳密に文字単位で比較します。<=>は両方のオペランドの関係を示す整数を返し、==は比較結果が0の場合、つまり両方の値が等しく比較された場合にtrueを返すことに注意してください。

ただし、大文字と小文字を区別しない比較を提供する#compareメソッドもあります。

message = "Hello World!"

p! message == "Hello World",
  message == "Hello Crystal",
  message == "hello world",
  message.compare("hello world", case_insensitive: false),
  message.compare("hello world", case_insensitive: true)

部分的なコンポーネント

文字列が別の文字列と完全に一致するかどうかを知ることが重要ではなく、単に一方の文字列が他方の文字列を含んでいるかどうかを知りたい場合があります。たとえば、#includes?メソッドを使用して、メッセージがCrystalに関するものかどうかを確認してみましょう。

message = "Hello World!"

p! message.includes?("Crystal"),
  message.includes?("World")

文字列の先頭または末尾が特に重要な場合があります。そのような場合に役立つのが#starts_with?#ends_with?メソッドです。

message = "Hello World!"

p! message.starts_with?("Hello"),
  message.starts_with?("Bye"),
  message.ends_with?("!"),
  message.ends_with?("?")

部分文字列のインデックス

#indexメソッドを使用すると、部分文字列の位置に関するさらに詳細な情報を取得できます。これは、部分文字列が最初に出現する箇所の最初の文字のインデックスを返します。結果が0の場合、starts_with?と同じ意味になります。

p! "Crystal is awesome".index("Crystal"),
  "Crystal is awesome".index("s"),
  "Crystal is awesome".index("aw")

このメソッドには、文字列の先頭以外の位置から検索を開始するために使用できるオプションのoffset引数があります。これは、部分文字列が複数回出現する可能性がある場合に便利です。

message = "Crystal is awesome"

p! message.index("s"),
  message.index("s", offset: 4),
  message.index("s", offset: 10)

#rindexメソッドは同じように動作しますが、文字列の末尾から検索します。

message = "Crystal is awesome"

p! message.rindex("s"),
  message.rindex("s", 13),
  message.rindex("s", 8)

部分文字列が見つからない場合、結果はnilと呼ばれる特別な値になります。これは「値がない」という意味です。部分文字列にインデックスがない場合は理にかなっています。

#indexの戻り型を見ると、Int32またはNilのいずれかを返すことがわかります。

a = "Crystal is awesome".index("aw")
p! a, typeof(a)
b = "Crystal is awesome".index("meh")
p! b, typeof(b)

ヒント

nilについては、次のレッスンで詳しく説明します。

部分文字列の抽出

部分文字列は、文字列の一部です。文字列の一部を抽出したい場合は、いくつかの方法があります。

インデックスアクセサ#[]を使用すると、文字インデックスとサイズによって部分文字列を参照できます。文字インデックスは0から始まり、長さ(つまり、#sizeの値)から1を引いた値までになります。最初の引数は部分文字列に含める最初の文字のインデックスを指定し、2番目の引数は部分文字列の長さを指定します。message[6, 5]は、インデックス6から始まる長さ5文字の部分文字列を抽出します。

message = "Hello World!"

p! message[6, 5]

文字列がHelloで始まり、!で終わることを確認できたとしましょう。そして、その間にあるものを抽出したいとします。メッセージがHello Crystalの場合、Crystalという単語全体は、5文字よりも長いため、取得できません。

解決策は、文字列全体の長さから先頭と末尾の長さを引いて、部分文字列の長さを計算することです。

message = "Hello World!"

p! message[6, message.size - 6 - 1]

もっと簡単な方法があります。インデックスアクセサは、文字インデックスのRangeと共に使用できます。範囲リテラルは、開始値と終了値で構成され、2つのドット(..)で接続されます。最初の値は、以前と同様に部分文字列の開始インデックスを示しますが、2番目は(長さではなく)終了インデックスです。これで、終了インデックスがサイズから2を引いたもの(終了インデックスの1つと最後の文字を除外するための1つ)であるため、計算で開始インデックスを繰り返す必要はありません。

さらに簡単にすることもできます。負のインデックス値は自動的に文字列の末尾に関連付けられるため、文字列サイズから終了インデックスを明示的に計算する必要はありません。

message = "Hello World!"

p! message[6..(message.size - 2)],
  message[6..-2]

置換

非常に似た方法で、文字列を変更できます。Crystalを適切に挨拶し、他には何も挨拶しないようにしましょう。部分文字列にアクセスする代わりに、#subを呼び出します。最初の引数は、再び、2番目の引数の値に置き換えられる場所を示す範囲です。

message = "Hello World!"

p! message.sub(6..-2, "Crystal")

#subメソッドは非常に汎用性が高く、さまざまな方法で使用できます。検索文字列を最初の引数として渡すこともでき、その部分文字列を2番目の引数の値に置き換えます。

message = "Hello World!"

p! message.sub("World", "Crystal")

#subは、検索文字列の最初のインスタンスのみを置き換えます。その兄弟である#gsubは、すべてのインスタンスに適用されます。

message = "Hello World! How are you, World?"

p! message.sub("World", "Crystal"),
  message.gsub("World", "Crystal")

ヒント

詳細については、文字列リテラルリファレンスString APIドキュメントをご覧ください。