Programmierung in Ruby

Der Leitfaden der Pragmatischen Programmierer

Standardtypen



Bis jetzt hatten wir Spaß am Implementieren von Codestücken für die Jukebox, aber wir waren nachlässig. Wir haben uns Arrays, Hashes und Procs angeschaut, aber wir haben die anderen grundlegenden Typen von Ruby nicht behandelt: Zahlen, Strings, Ranges und reguläre Ausdrücke. Lasst uns einige Seiten für diese grundlegenden Bausteine aufwenden.

Zahlen

Ruby unterstützt Integer- und Fließkommawerte. Integers (ganze Zahlen) können jede Länge haben (bis zu einem Maximum, das vom vorhandenen freien Speicher Ihres System bestimmt wird). Integers innerhalb eines bestimmten Bereichs (normalerweise -230 bis 230-1 oder -262 bis 262-1) werden intern in binärer Form verwaltet und sind Objekte der Klasse Fixnum. Integers außerhalb dieses Bereichs sind Objekte der Klasse Bignum (die zur Zeit als variable Menge von kurzen Integerwerten implementiert ist). Dieser Prozess ist transparent und Ruby verwaltet die Konvertierung zwischen den beiden Klassen automatisch.

num = 8
7.times do
  print num.type, " ", num, "\n"
  num *= num
end
ergibt:
Fixnum 8
Fixnum 64
Fixnum 4096
Fixnum 16777216
Bignum 281474976710656
Bignum 79228162514264337593543950336
Bignum 6277101735386680763835789423207666416102355444464034512896

Integers werden mit optionalem Vorzeichen und optionaler Basis (0 für oktale, 0x für hexadezimale oder 0b für binäre Werte) gefolgt von einer Zeichenkette von Zahlen zur passenden Basis geschrieben. Unterstriche werden dabei ignoriert.

123456                    # Fixnum
123_456                   # Fixnum (Unterstrich ignoriert)
-543                      # Negatives Fixnum
123_456_789_123_345_789   # Bignum
0xaabb                    # Hexadezimal
0377                      # Oktal
-0b101_010                # Binär (negiert)

Sie können auch den Integerwert eines ASCII-Zeichens oder einer Escapesequenz bestimmen, indem Sie dem Wert ein Fragezeichen voranstellen. Auch Kombinationen mit den Tasten Control und Meta können Sie erzeugen, indem Sie ?\C-x, ?\M-x und ?\M-\C-x verwendet. Die ``Control''-Version einer Wertes ist dasselbe wie ``value & 0x9f'', die ``Meta''-Version dasselbe wie value | 0x80''. Schlussendlich erzeugt ?\C-? ein ``ASCII Delete'' (Löschzeichen), 0177.

?a                        # Zeichencode
?\n                       # Zeichencode eines Newlines (0x0a)
?\C-a                     # Control a = ?A & 0x9f = 0x01
?\M-a                     # Meta setzt Bit 7
?\M-\C-a                  # Meta und Control a
?\C-?                     # Deletezeceichen

Eine Zahl mit Dezimalpunkt und/oder Exponent wird, entsprechend dem Datentyp double der verwendeten Architektur, in ein Float-Objekt umgewandelt. Dem Dezimalpunkt muss eine Ziffer folgen, da ansonsten 1.e3 versuchen würde, die Methode e3 der Klasse Fixnum aufzurufen.

Alle Zahlen sind Objekte und reagieren auf eine Vielfalt von Nachrichten (die vollständige Auflistung finden Sie ab den Seiten 294, 318, 319, 328 und 354). Somit können Sie, anders als in (sagen wir) C++, den Absolutwert einer Zahl mittels eineZahl.abs herausfinden und nicht mit abs(eineZahl).

Integers unterstützen auch eine Reihe nützlicher Iteratoren. Einen haben wir bereits gesehen -- 7.times im Codebeispiel auf Seite 49. Andere sind etwa upto und downto, mit denen man über zwei Integerwerten auf- und abwärts iterieren kann, sowie step, das eher wie eine herkömmliche for-Schleife arbeitet.

3.times        { print "X " }
1.upto(5)      { |i| print i, " " }
99.downto(95)  { |i| print i, " " }
50.step(80, 5) { |i| print i, " " }
erzeugt:
X X X 1 2 3 4 5 99 98 97 96 95 50 55 60 65 70 75 80

Schlussendlich eine Warnung für Perl-Benutzer: Strings, die Zahlen beinhalten werden nicht automatisch in Zahlen konvertiert, wenn Sie innerhalb von Ausdrücken benutzt werden. Daran beißt man sich wahrscheinlich am ehesten, wenn man Zahlen aus einer Datei einliest. Der folgende Code tut (wahrscheinlich) nicht das, was beabsichtigt war:

DATA.each do |line|
  vals = line.split    # Aufspalten der Zeile, Zeichen in vals speichern.
  print vals[0] + vals[1], " "
end

Füttern Sie diesem Programm folgende Datei

3 4
5 6
7 8

und Sie erhalten die Ausgabe ``34 56 78.'' Was ist geschehen?

Das Problem ist, dass die Eingabe als Strings und nicht als Zahlen gelesen wurde. Der Plusoperator verkettet Strings, das ist es, was wir in der Ausgabe sehen. Um das zu reparieren, verwenden Sie die Methode String#to_i, um den String in eine Zahl zu konvertieren.

DATA.each do |line|
  vals = line.split
  print vals[0].to_i + vals[1].to_i, " "
end
ergibt:
7 11 15

Zeichenketten (Strings)

Zeichenketten in Ruby sind einfache Sequenzen von 8-bittigen Bytes. Normalerweise speichern Sie druckbare Zeichen, das ist aber keine Voraussetzung; eine String kann auch binäre Daten enthalten. Zeichenketten sind Objekte der Klasse String.

Strings werden oft mittels Zeichenkettensymbolen erzeugt -- Sequenzen von Buchstaben zwischen Begrenzern. Da es ansonsten schwer wäre, binäre Daten innerhalb der Programmsourcen zu repräsentieren, können Sie innerhalb von Strings verschiedene Escapesequenzen verwenden. Jedes Einzelne wird während des Kompilierens mit dem entsprechenden Binärwert ersetzt. Der verwendete Begrenzer bestimmt den Grad der Ersetzungen: Innerhalb von einfachen Anführungszeichen werden zwei aufeinanderfolgende Backslashes durch einen einzelnen ersetzt und ein Backslash mit einfachem Anführungszeichen wird zu einem einfachen '.

'escape using "\\"' » escape using "\"
'That\'s right' » That's right

Strings innerhalb von doppelten Anführungszeichen unterstützen eine Schiffsladung mehr an Escapesequenzen. Die geläufigste ist vermutlich ``\n'', das Newline-Zeichen. Tabelle 18.2 auf Seite 205 zeigt die komplette Liste. Zusätzlich können Sie mit der Sequenz #{ ausdruck } den Wert jeder Ruby-Anweisung in einen String einsetzen. Wenn der Ausdruck nur eine globale Variable, eine Klassenvariable oder eine Instanzvariable ist, können Sie die geschwungenen Klammern auslassen.

"Seconds/day: #{24*60*60}" » Seconds/day: 86400
"#{'Ho! '*3}Merry Christmas" » Ho! Ho! Ho! Merry Christmas
"This is line #$." » This is line 3

Es gibt drei weitere Wege, um Strings zu erstellen: %q, %Q und ``Here-Dokumente.''

%q und %Q verhalten sich wie einfache oder doppelte Anführungszeichen.

%q/String wie mit einfachen Anführungszeichen/ » String wie mit einfachen Anführungszeichen
%Q!String wie mit doppelten Anführungszeichen! » String wie mit doppelten Anführungszeichen
%Q{Sekunden/Tag: #{24*60*60}} » Sekunden/Tag: 86400

Das Zeichen, das dem ``q'' oder dem ``Q'' folgt, ist der Begrenzer. Ist das {, [, ( oder < wird der String bis zum nächsten passenden Symbol (also >, ), ] oder }) gelesen. Andernfalls wird der String bis zum nächsten Vorkommen desselben Begrenzers gelesen.

Schlussendlich können Sie Strings mit einem Here-Dokument erzeugen.

aString = <<ENDE_DES_STRINGS
    Der String besteht aus
    allen Eingabezeilen bis
    zur nächsten, die mit
    dem Text, der auf '<<'
    folgte, endet.
ENDE_DES_STRINGS

Ein Here-Dokument besteht aus allen Zeilen im Sourcecode, bis zum String, den Sie nach den Zeichen << angeben (der aber nicht inkludiert wird). Normalerweise muss dieser Begrenzer in der ersten Spalten stehen. Wenn Sie jedoch nach dem << ein Minuszeichen schreiben, können Sie den Begrenzer einrücken.

print <<-STRING1, <<-STRING2
   ver
   STRING1
      ketten
      STRING2
erzeugt:
     ver
        ketten

Mit Strings arbeiten

String ist mit mehr als 75 Standardmethoden vermutlich die größte in Ruby eingebaute Klasse. Wir werden hier nicht alle vorstellen; die Bibliotheksreferenz bietet eine komplette Liste. Stattdessen schauen wir uns einige geläufige Eigentümlichkeiten von Strings an -- Dinge, auf die Sie während des tagtäglichen Programmierens wahrscheinlich treffen werden.

Gehen wir zurück zu unserer Jukebox. Obwohl sie entworfen wurde, um mit dem Internet verbunden zu sein, speichert sie auch Kopien einiger beliebter Songs auf einer lokalen Festplatte. Auch wenn ein Eichhörnchen sich durch unsere Netzverbindung schraubt, ist es uns auf diese Weise trotzdem möglich, unsere Kunden zu unterhalten.

Aus historischen Gründen (gibt's überhaupt andere?) ist die Liste der Songs in Zeilen in einer normalen Datei gespeichert. Jede Zeile enthält den Namen der Datei, in dem der Song gespeichert ist, die Länge des Songs, den Künstler und den Titel, alles in durch vertikale Striche getrennten Feldern. Eine typische Datei könnte so beginnen:

/jazz/j00132.mp3  | 3:45 | Fats     Waller     | Ain't Misbehavin'
/jazz/j00319.mp3  | 2:58 | Louis    Armstrong  | Wonderful World
/bgrass/bg0732.mp3| 4:09 | Strength in Numbers | Texas Red
         :                  :           :                   :

Wenn wir uns die Daten anssehen, wird es klar, dass wir einige der vielen Methoden der Klasse String verwenden werden, um die Felder zu extrahieren und zu säubern, bevor wir ein darauf beruhendes Objekt von Song erstellen können. Zumindest müssen wir:

Unsere erste Aufgabe ist, jede Zeile in Felder aufzutrennen und String#split wird diese Arbeit gut verrichten. In unserem Fall übergeben wir split den regulären Ausdruck /\s*\|\s*/, der die Zeile immer dann aufspaltet, wenn split einen vertikalen Strich findet, der optional von Leerzeichen umgeben ist. Und da die Zeile, die aus der Datei gelesen wird, immer noch ein Newline enthält, verwenden wir String#chomp, um es abzuschneiden, bevor wir split anwenden.

songs = SongList.new

songFile.each do |line|   file, length, name, title = line.chomp.split(/\s*\|\s*/)   songs.append Song.new(title, name, length) end puts songs[1]
ergibt:
Song: Wonderful World--Louis    Armstrong (2:58)

Unglücklicherweise hat, wer auch immer die Datei erstellt hat, die Namen der Künstler in Spalten eingegeben, weshalb einige davon überflüssige Leerzeichen enthalten. Das schaut auf unserem todschicken high-tech Display hässlich aus, daher entfernen wir diese Leerzeichen lieber, bevor wir weitermachen. Es gibt viele Möglichkeiten, das zu machen, die einfachste ist wahrscheinlich aber die Methode String#squeeze, die wiederholte Zeichen wegschneidet. Wir verwenden die Form squeeze!, die den String auf der Stelle verändert.

songs = SongList.new

songFile.each do |line|   file, length, name, title = line.chomp.split(/\s*\|\s*/)   name.squeeze!(" ")   songs.append Song.new(title, name, length) end puts songs[1]
ergibt:
Song: Wonderful World--Louis Armstrong (2:58)

Schlussendlich ist da die Nebensache mit dem Zeitformat: In der Datei steht 2:58, wir wollen die Zeit aber in Sekunden: 178. Wir könnten wieder split benutzen und diesmal die Felder um den Doppelpunkt herum ausschneiden.

mins, secs = length.split(/:/)

Stattdessen verwenden wir aber eine verwandte Methoden. String#scan ist split ähnlich, da es Strings anhand von Mustern in Stücke schneidet. Anders als split gibt man bei scan aber das Muster an, das man erhalten will. In diesem Fall, möchten wir eine Ziffer oder mehrere Ziffern für die Minuten und die Sekunden finden. Das Muster für mindestens eine Ziffer ist /\d+/.

songs = SongList.new
songFile.each do |line|
  file, length, name, title = line.chomp.split(/\s*\|\s*/)
  name.squeeze!(" ")
  mins, secs = length.scan(/\d+/)
  songs.append Song.new(title, name, mins.to_i*60+secs.to_i)
end
puts songs[1]
ergibt:
Song: Wonderful World--Louis Armstrong (178)

Unsere Jukebox hat eine eingebaute Suchfunktion. Gibt man ein Wort aus einem Songtitel oder aus einem Namen eines Künstlers ein, listet sie alle passenden Lieder auf. Geben Sie ``fats'' ein und sie könnte Ihnen zum Beispiel Songs von Fats Domino, Fats Navarro und Fats Waller ausgeben. Wir implementieren diese Funktion, indem wir eine Indexklasse erstellen. Füttert man diese mit einem Objekt und einigen Strings, so erstellt sie einen Index anhand jedes Wortes (mit zwei oder mehr Buchstaben), das in den Zeichenketten vorkommt. Dieses Beispiel zeigt uns wieder einige der vielen Methoden der Klasse String.

class WordIndex
  def initialize
    @index = Hash.new(nil)
  end
  def index(anObject, *phrases)
    phrases.each do |aPhrase|
      aPhrase.scan /\w[-\w']+/ do |aWord|   # jedes Wort extrahieren
        aWord.downcase!
        @index[aWord] = [] if @index[aWord].nil?
        @index[aWord].push(anObject)
      end
    end
  end
  def lookup(aWord)
    @index[aWord.downcase]
  end
end

Die Methode String#scan extrahiert Elemente aus einem String, die auf einen regulären Ausdruck passen. In diesem Fall findet das Muster ``\w[-\w']+'' jedes Zeichen, das in einem Wort vorkommen kann, gefolgt von einem oder mehreren der Dinge die in den eckigen Klammern angegeben sind (ein Bindestrich, noch ein Wortzeichen und ein einfaches Anführungszeichen). Wir erläutern reguläre Ausdrücke ab Seite 58 näher. Damit unsere Suche nicht zwischen Klein- und Großbuchstaben unterscheidet, wandeln wir sowohl die Wörter, die wir extrahieren als auch die Wörter, die wir für unsere Suche verwenden, in Kleinbuchstaben um. Beachten Sie das Rufzeichen am Ende der ersten downcase!-Methode. Wie auch in der Methode squezze!, die wir vorhin benutzt haben, ist das ein Zeichen dafür, dass die Methode den Empfänger auf der Stelle verändern wird, in unserem Fall also den String in Kleinbuchstaben umwandelt.[In diesem Codebeispiel ist ein kleiner Bug: Das Lied ``Gone, Gone, Gone'' würde drei Mal in den Index aufgenommen werden. Können Sie das in Ordnung bringen?]

Wir erweitern unsere Klasse SongList, damit sie Songs schon beim Hinzufügen in den Index aufnimmt und fügen auch eine Methode ein, mit der man mit einem gegebenen Wort nach einem Lied suchen kann.

class SongList
  def initialize
    @songs = Array.new
    @index = WordIndex.new
  end
  def append(aSong)
    @songs.push(aSong)
    @index.index(aSong, aSong.name, aSong.artist)
    self
  end
  def lookup(aWord)
    @index.lookup(aWord)
  end
end

Schlussendlich testen wir alles.

songs = SongList.new
songFile.each do |line|
  file, length, name, title = line.chomp.split(/\s*\|\s*/)
  name.squeeze!(" ")
  mins, secs = length.scan(/\d+/)
  songs.append Song.new(title, name, mins.to_i*60+secs.to_i)
end
puts songs.lookup("Fats")
puts songs.lookup("ain't")
puts songs.lookup("RED")
puts songs.lookup("WoRlD")
ergibt:
Song: Ain't Misbehavin'--Fats Waller (225)
Song: Ain't Misbehavin'--Fats Waller (225)
Song: Texas Red--Strength in Numbers (249)
Song: Wonderful World--Louis Armstrong (178)

Wir könnten die nächsten 50 Seiten damit zubringen, uns die Methoden der Klasse String anzusehen. Schauen wir uns aber stattdessen einen einfacheren Datentyp an: Ranges.

Bereiche (Ranges)

Bereiche tauchen überall auf: Jänner bis Dezember, 0 bis 9, Genügend bis Sehr Gut, die Zeilen 50 bis 67 und so weiter. Da Ruby uns hilft, die Wirklichkeit nachzubauen, scheint es ganz natürlich zu sein, dass die Sprache auch Bereiche unterstützt. Tatsächlich geht Ruby noch einen Schritt weiter: Es verwendet Ranges sogar um drei separate Merkmale zu implementieren: Reihen, Bedingungen und Intervalle.

Ranges als Reihen

Der erste und vielleicht natürlichste Verwendungszweck von Bereichen ist es, Reihen auszudrücken. Reihen haben einen Start- und einen Endpunkt und eine Art und Weise um aufeinanderfolgende Werte innerhalb dieser Punkte zu erzeugen. In Ruby werden diese Sequenzen mit den Bereichsoperatoren ``..'' und ``...'' erstellt. Die Form mit den zwei Punkten erzeugt eine inklusive Reihe, während die dreipunktige Form einen Bereich erzeugt, in dem der angegebene obere Endpunkt nicht mehr enthalten ist.

1..10
'a'..'z'
0...anArray.length

In Ruby werden, anders als in manchen früheren Versionen von Perl, Ranges intern nicht als Listen repräsentiert: Die Sequenz 1..100000 wird intern als Objekt der Klasse Range gespeichert, die Referenzen auf zwei Objekte von Fixnum enthält. Wenn Sie müssen, können Sie einen Bereich mit der Methode to_a in eine Liste umwandeln.

(1..10).to_a » [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
('bar'..'bat').to_a » ["bar", "bas", "bat"]

Ranges führen Methoden mit, die es Ihnen erlauben, über die Elemente zu iteratieren und ihren Inhalt auf verschiedene Arten zu überprüfen.

digits = 0..9
digits.include?(5) » true
digits.min » 0
digits.max » 9
digits.reject {|i| i < 5 } » [5, 6, 7, 8, 9]
digits.each do |digit|
  dial(digit)
end

Bis jetzt haben wir Ranges von Zahlen und Strings gezeigt. Wie Sie jedoch von einer objektorientierten Sprache erwarten, kann Ruby auch Bereiche erstellen, die auf Objekten basieren, die Sie definiert haben. Der einzige Zwang dabei ist, dass die Objekte auf die Methode succ reagieren müssen und dabei das nächste Objekt in der Reihe zurückgeben. Weiter muss das Objekt mit dem allgemeinen Vergleichsoperator <=> vergleichbar sein. Dieser Operator, manchmal auch Raumschiffoperator (Spaceship Operator) genannt, gibt -1, 0 oder 1 zurück, je nachdem, ob der erste Wert kleiner, gleich oder größer als der zweite Wert ist.

Hier eine einfache Klasse, die Zeilen mit ``#''-Zeichen repräsentiert. Wir könnten Sie als textbasierte Kontrolle benutzen, wenn wir die Lautstärkenfunktion unserer Jukebox ausprobieren.

class VU

  include Comparable

  attr :volume

  def initialize(volume)  # 0..9     @volume = volume   end

  def inspect     '#' * @volume   end

  # Support for ranges

  def <=>(other)     self.volume <=> other.volume   end

  def succ     raise(IndexError, "Volume too big") if @volume >= 9     VU.new(@volume.succ)   end end

Wir können sie testen, indem wir einen Bereich von VU-Objekten bilden:

medium = VU.new(4)..VU.new(7)
medium.to_a » [####, #####, ######, #######]
medium.include?(VU.new(3)) » false

Bereiche als Bedingungen

Ranges können sowohl zum Repräsentieren von Reihen als auch als Bedingungen verwendet werden. Der folgende Code beispielsweise gibt Mengen von Zeilen der Standardeingabe aus, wobei die erste Zeile in jeder Menge das Wort ``start'' enthält und die letzte Zeile das Wort ``end''.

while gets
  print if /start/../end/
end

Hinter den Kulissen hält sich der Bereich über die Status der einzelnen Tests auf dem Laufenden. Wir zeigen davon einige Beispiele in der Beschreibung von Schleifen, die auf Seite 84 beginnt.

Bereiche als Intervalle

Ein letzter Verwendungszweck der vielseitigen Ranges ist der Intervalltest: Dabei wird überprüft, ob ein Wert innerhalb eines Intervalls liegt, das von einem Bereich repräsentiert wird. Dies geschieht mit ===, dem case equality operator.

(1..10)    === 5 » true
(1..10)    === 15 » false
(1..10)    === 3.14159 » true
('a'..'j') === 'c' » true
('a'..'j') === 'z' » false

Das Beispiel eines case-Ausdrucks auf Seite 83 zeigt den Einsatz dieses Tests. Dabei wird anhand eines gegebenen Jahres der Stil eines Jazzsongs festgestellt wird.

Reguläre Ausdrücke

Auf Seite 53 verwendeten wir, als wir eine Songliste aus einer Datei erstellten, einen regulären Ausdruck, um die Trennzeichen in der Eingabedatei zu finden. Wir behaupteten, das der Ausdruck line.split(/\s*\|\s*/) einen mit optionalen Leerzeichen umgebenen senkrechten Strich findet. Erforschen wir nun die Details regulärer Ausdrücke, um festzustellen, warum diese Behauptung wahr ist.

Reguläre Ausdrücke werden verwendet um ein Muster in einem String zu finden. Rubys eingebaute Unterstützung erlaubt es, Muster bequem und prägnant zu suchen und zu ersetzen. In diesem Abschnitt erarbeiten wir alle wichtigen Eigenschaften regulärer Ausdrücke. Einige Details werden wir nicht besprechen: schauen Sie für weitere Informationen auf Seite 207.

Reguläre Ausdrücke sind Objekte vom Type Regexp. Sie können erstellt werden, indem man den Konstruktor explizit aufruft oder die Abkürzungen in den Formen /muster/ und %r\muster\ verwendet.

a = Regexp.new('^\s*[a-z]') » /^\s*[a-z]/
b = /^\s*[a-z]/ » /^\s*[a-z]/
c = %r{^\s*[a-z]} » /^\s*[a-z]/

Haben Sie einmal ein Objekt eines regulären Ausdrucks erstellt, können Sie das Muster mit Regexp#match(einString) oder den Suchoperatoren =~ (positive Suche) oder !~ (negierte Suche) in einem String suchen. Die Suchoperatoren sind sowohl für Objekte von String als auch für Objekte von Regexp definiert. Sind beide Operanden des Suchoperators vom Typ String, wird der rechte Operand in einen regulären Ausdruck umgewandelt.

a = "Fats Waller"
a =~ /a/ » 1
a =~ /z/ » nil
a =~ "ll" » 7

Die Suchoperatoren geben die Position des Zeichens zurück, an der das Muster gefunden wurde. Als Seiteneffekt setzten sie eine ganze Ladung von Ruby-Variablen. $& enthält den Teil des Strings, in dem das Muster gefunden wurde, $` enthält den Teil, der der Fundstelle voranging und $' den String, der nach der Fundstelle kam. Das können wir verwenden, um eine Methode showRE zu schreiben, die zeigt, wo ein bestimmtes Muster gefunden wurde.

def showRE(a,re)
  if a =~ re
    "#{$`}<<#{$&}>>#{$'}"
  else
    "no match"
  end
end
showRE('very interesting', /t/) » very in<<t>>eresting
showRE('Fats Waller', /ll/) » Fats Wa<<ll>>er

Ein Fund setzt auch die für Threads globalen Variablen $~ und $1 bis $9. Die Variable $~ ist ein Objekt der Klasse MatchData (die ab Seite 340 beschrieben wird), die alles enthält, was Sie vielleicht über einen Suchvorgang wissen möchten. $1 und so weiter speichern Teile des gefundenen Musters. Darüber sprechen wir später. Und Leuten, die sich krümmen, wenn sie diese an Perl erinnernden Variablen sehen, sei gesagt: halten Sie sich bereit. Am Ende dieses Kapitels gibt's gute Nachrichten.

Muster

Jeder reguläre Ausdruck enthält ein Muster, der verwendet wird, um den regulären Ausdruck in einem String zu finden.

Innerhalb eines Musters finden sich alle Buchstaben außer ., |, (, ), [, {, +, \, ^, $, *, und ? selbst.

showRE('kangaroo', /angar/) » k<<angar>>oo
showRE('!@%&-_=+', /%&/) » !@<<%&>>-_=+

Möchten Sie eines dieser Spezialzeichen selbst finden, müssen Sie ihm einen Backslash voranstellen. Das erklärt einen Teil des Musters /\s*\|\s*/, das wir verwendet haben, um eine Songzeile aufzuteilen. \| bedeutet ``finde einen senkrechten Strich''. Ohne Backslash würde ``|'' Alternation bedeuten (wir beschreiben das später).

showRE('yes | no', /\|/) » yes <<|>> no
showRE('yes (no)', /\(no\)/) » yes <<(no)>>
showRE('are you sure?', /e\?/) » are you sur<<e?>>

Ein Backslash gefolgt von einem alphanumerischen Zeichen leitet ein spezielles Suchkonstrukt ein, das wir später behandeln werden. Zusätzlich kann ein regulärer Ausdruck Ausdrücke in #{...}-Form enthalten.

Anker

Von Haus aus versucht ein regulärer Ausdruck den ersten auf das Muster passenden Teilstring in einer Zeichenkette zu finden. Wenn Sie /iss/ im String ``Mississippi'' suchen, wird der Substring ``iss'', der an Position 1 beginnt, gefunden. Was aber, wenn Sie ein Muster zwingen wollen nur den Anfang oder das Ende eines Strings zu finden?

Die Muster ^ und $ findet den Anfang bzw. das Ende einer Zeile. Diese Muster werden häufig benutzt, um einen Anker in einem Suchmuster zu setzen: /^option/ beispielsweise findet das Wort ``option'' nur, wenn es am Beginn einer Zeile steht. \A findet den Anfang, und \z und \Z das Ende eines Strings. (Eigentlich findet \Z das Ende eines Strings, außer der String endet auf ``\n''. In diesem Fall findet es einfach das Zeichen vor dem ``\n''.)

showRE("this is\nthe time", /^the/) » this is\n<<the>> time
showRE("this is\nthe time", /is$/) » this <<is>>\nthe time
showRE("this is\nthe time", /\Athis/) » <<this>> is\nthe time
showRE("this is\nthe time", /\Athe/) » no match

Ähnlich funktionieren die Muster \b und \B, die Wortgrenzen bzw. keine Wortgrenzen finden. Wortzeichen sind Buchstaben, Zahlen und der Unterstrich.

showRE("this is\nthe time", /\bis/) » this <<is>>\nthe time
showRE("this is\nthe time", /\Bis/) » th<<is>> is\nthe time

Zeichenklassen

Eine Zeichenklasse ist eine Menge von Zeichen zwischen eckigen Klammern: [Zeichen] findet jedes einzelne Zeichen, das zwischen den Klammern steht. [aeiou] findet einen Vokal, [,.:;!?] findet Satzzeichen usw. Die Bedeutung der speziellen Zeichen in regulären Ausdrücken -- .|()[}+^$*? -- ist innerhalb der Klammern abgeschaltet. Normale Stringersetzung tritt jedoch ein, somit steht (zum Beispiel) \b für ein Backspace und \n für ein Newline-Zeichen (siehe Tabelle 18.2 auf Seite 205). Zusätzlich können Sie die Abkürzungen verwenden, die in Tabelle 5.1 auf Seite 62 gezeigt werden, sodass (zum Beispiel) \s jedes Whitespace-Zeichen (also Leerzeichen, Tabulator, Neue Zeile usw.) und nicht nur ein Leerzeichen findet.

showRE('It costs $12.', /[aeiou]/) » It c<<o>>sts $12.
showRE('It costs $12.', /[\s]/) » It<< >>costs $12.

Innerhalb der eckigen Klammern findet die Sequenz c1-c2 alle Zeichen zwischen c1 und c2 (inklusive der Grenzen).

Möchten Sie die Zeichen ] und - innerhalb von Zeichenklassen verwenden, so müssen diese am Beginn stehen.

a = 'Gamma [Design Patterns-page 123]'
showRE(a, /[]]/) » Gamma [Design Patterns-page 123<<]>>
showRE(a, /[B-F]/) » Gamma [<<D>>esign Patterns-page 123]
showRE(a, /[-]/) » Gamma [Design Patterns<<->>page 123]
showRE(a, /[0-9]/) » Gamma [Design Patterns-page <<1>>23]

Um eine Zeichenklasse zu negieren, müssen Sie gleich nach der öffnenden Klammer ^ schreiben: [^a-z] findet jedes Zeichen, das kein Kleinbuchstabe ist.

Einige Zeichenklassen kommen so häufig vor, dass Ruby Abkürzungen für sie bereitstellt, die Sie in Tabelle 5.1 auf Seite 62 finden können. Diese Abkürzungen können sowohl in Zeichenklassen als auch innerhalb von Suchmustern selbst vorkommen.

showRE('It costs $12.', /\s/) » It<< >>costs $12.
showRE('It costs $12.', /\d/) » It costs $<<1>>2.

Abkürzungen für Zeichenklasse
Sequenz Als [ ... ] Bedeutung
\d [0-9] Alle Ziffern
\D [^0-9] Alle Zeichen außer Ziffern
\s [\s\t\r\n\f] Whitespace-Zeichen (Leerzeichen, Tabulator, Wagenrücklauf, Neue Zeile, Blattvorschub)
\S [^\s\t\r\n\f] Alle Zeichen außer Whitespace-Zeichen
\w [A-Za-z0-9_] Wortzeichen (alphanumerisch und _)
\W [^A-Za-z0-9_] Alle Zeichen außer Wortzeichen

Schlussendlich findet ein Punkt (``.''), der außerhalb von eckigen Klammern auftaucht, jedes Zeichen außer ein Newline (und im Mehrzeilenmodus findet sogar auch ein Newline).

a = 'It costs $12.'
showRE(a, /c.s/) » It <<cos>>ts $12.
showRE(a, /./) » <<I>>t costs $12.
showRE(a, /\./) » It costs $12<<.>>

Wiederholung

Als wir das Muster /\s*\|\s*/ angegeben haben, das eine Zeile aus der Liederdatei aufspalten sollte, sagten wir, dass wir damit einen senkrechten Strich finden wollten, der von einer unbestimmten Zahl von Leerzeichen umgeben ist. Wir wissen nun, dass \s ein einzelnes Whitespace-Zeichen findet, somit scheint es wahrscheintlich zu sein, dass ein Stern ``eine unbestimmte Zahl'' bedeutet. Tatsächlich ist der Stern eines der Zeichen, die es erlauben, mehrere Vorkommen eines Musters zu finden.

Wenn r für den unmittelbar zuvor stehenden regulären Ausdruck innerhalb einer Musters steht, dann findet:

r* null oder mehr Vorkommen von r.
r+ ein oder mehr Vorkommen von r.
r? null oder ein Vorkommen von r.
r{m,n} mindestens ``m'' und höchstens ``n'' Vorkommen von r.
r{m,} mindestens ``m'' Vorkommen von r.

Diese Wiederholungskonstrukte haben eine hohe Rangfolge -- sie gelten nur für den unmittelbar davor stehenden regulären Ausdruck im Muster. /ab+/ findet ein ``a'' gefolgt ein einem oder mehreren ``b''s, keine Sequenz von ``ab''s. Gehen Sie auch mit *-Konstrukten vorsichtig um -- das Muster /a*/ findet jeden String; in jedem String kommen null oder mehr ``a''s vor.

Diese Mustern werden gierig genannt, da sie, sofern nicht anders angegeben, so viel wie möglich vom String zurückliefern. Dieses Verhalten können Sie ändern, indem Sie ein Fragezeichen anhängen, damit der reguläre Ausdruck nur den kleinstmöglichen Teilstring findet.

a = "The moon is made of cheese"
showRE(a, /\w+/) » <<The>> moon is made of cheese
showRE(a, /\s.*\s/) » The<< moon is made of >>cheese
showRE(a, /\s.*?\s/) » The<< moon >>is made of cheese
showRE(a, /[aeiou]{2,99}/) » The m<<oo>>n is made of cheese
showRE(a, /mo?o/) » The <<moo>>n is made of cheese

Alternation

Wir wissen, dass der senkrechte Strich ein besondere Bedeutung hat, da ihm in unserem Pattern zum Aufteilen der Zeile ein Backslash vorangestellt wurde: Das geschah, weil ein einfacher Strich ``|'' entweder den regulären Ausdruck links von ihm oder den rechts von ihm findet.

a = "red ball blue sky"
showRE(a, /d|e/) » r<<e>>d ball blue sky
showRE(a, /al|lu/) » red b<<al>>l blue sky
showRE(a, /red ball|angry sky/) » <<red ball>> blue sky

Hier ist eine Falle für Unachtsame, da ``|'' niedrige Rangfolge hat. Das letzte obige Beispiel findet ``red ball'' oder ``angry sky'', nicht aber ``red ball sky'' oder ``red angry sky''. Um ``red ball sky'' oder ``red angry sky'' zu finden, müssen Sie die Ausdrücke Gruppieren, um die normale Rangfolge zu umgehen.

Gruppierung

Sie können Klammern verwenden, um Terme innerhalb eines regulären Ausdrucks zu gruppieren. Alles innerhalb der Gruppe wird wie ein einzelner regulärer Ausdruck behandelt.

showRE('banana', /an*/) » b<<an>>ana
showRE('banana', /(an)*/) » <<>>banana
showRE('banana', /(an)+/) » b<<anan>>a

a = 'red ball blue sky'
showRE(a, /blue|red/) » <<red>> ball blue sky
showRE(a, /(blue|red) \w+/) » <<red ball>> blue sky
showRE(a, /(red|blue) \w+/) » <<red ball>> blue sky
showRE(a, /red|blue \w+/) » <<red>> ball blue sky

showRE(a, /red (ball|angry) sky/) » no match
a = 'the red angry sky'
showRE(a, /red (ball|angry) sky/) » the <<red angry sky>>

Klammern werden auch benutzt, um die Ergebnisse eines Suchvorgangs zu speichern. Ruby zählt die öffnenden Klammern und speichert für jede den String, der bei der Suche zwischen ihr und ihrer entsprechenden schließenden Klammer gefunden wurde. Sie können diesen gespeicherten Teilwert sowohl innerhalb des verbleibenden Musters als auch in ihrem Ruby-Programm verwenden. Innerhalb des Musters können Sie mit \1 auf die erste Gruppe, mit \2 auf die zweite Gruppe usw. verweisen. Außerhalb des Musters dienen die speziellen Variablen $1, $2 usw. dem selben Zweck.

"12:50am" =~ /(\d\d):(\d\d)(..)/ » 0
"Hour is #$1, minute #$2" » "Hour is 12, minute 50"
"12:50am" =~ /((\d\d):(\d\d))(..)/ » 0
"Time is #$1" » "Time is 12:50"
"Hour is #$2, minute #$3" » "Hour is 12, minute 50"
"AM/PM is #$4" » "AM/PM is am"

Die Möglichkeit, einen Teil der aktuellen Suche später in derselben Suche wieder zu verwenden erlaubt es, nach verschiedensten Formen von Wiederholungen zu suchen.

# match duplicated letter
showRE('He said "Hello"', /(\w)\1/) » He said "He<<ll>>o"
# match duplicated substrings
showRE('Mississippi', /(\w+)\1/) » M<<ississ>>ippi

Sie können Rückbezüge auch verwenden, um Begrenzer zu finden.

showRE('He said "Hello"', /(["']).*?\1/) » He said <<"Hello">>
showRE("He said 'Hello'", /(["']).*?\1/) » He said <<'Hello'>>

Musterbasierte Ersetzung

Manchmal reicht es aus, ein Muster in einem String zu finden. Wenn Sie ein Freund herausfordert, ein Wort zu finden, das die Buchstaben a, b, c, d und e geordnet enthält könnten Sie eine Wörterliste mit dem Muster /a.*b.*c.*d.*e/ durchsuchen und ``absconded'' oder ``ambuscade'' finden. Das muss doch etwas wert sein.

Manchmal kommt es jedoch vor, dass Sie Dinge anhand eines Musters ändern müssen. Gehen wir zurück zu unserer Datei mit der Songliste. Wer auch immer sie erstellte, hat die Namen der Künstler in Kleinbuchstaben eingebeben. Wenn wir sie aber am Display unserer Jukebox anzeigen, würden Sie aber in gemischter Schreibweise besser aussehen. Wie können wir nun den ersten Buchstaben jedes Wortes in einen Großbuchstaben ändern?

Die Methoden String#sub und String#gsub suchen nach einem Teil eines Strings, der auf ihr erstes Argument passt und ersetzen ihn mit ihrem zweiten Argument String#sub führt nur eine Änderung durch, während String#gsub jedes Vorkommen des Musters ersetzt. Beide Routinen geben ein Objekt von String mit der ersetzen Version des Strings zurück. Die Versionen String#sub! und String#gsub! verändern den ursprünglichen String.

a = "the quick brown fox"
a.sub(/[aeiou]/,  '*') » "th* quick brown fox"
a.gsub(/[aeiou]/, '*') » "th* q**ck br*wn f*x"
a.sub(/\s\S+/,  '') » "the brown fox"
a.gsub(/\s\S+/, '') » "the"

Das zweite Argument beider Funktionen kann entweder ein String oder ein Block sein. Wenn ein Block verwendet wird, wird der String mit dem Wert des Blocks ersetzt.

a = "the quick brown fox"
a.sub(/^./) { $&.upcase } » "The quick brown fox"
a.gsub(/[aeiou]/) { $&.upcase } » "thE qUIck brOwn fOx"

Das schaut also wie die Antwort auf die Frage aus, wie wir die Namen unserer Künstler konvertieren. Das Muster, das das erste Zeichen jedes Wortes findet, ist \b\w -- such' nach einer Wortgrenze gefolgt von einem Wortzeichen. Wir kombinieren das mit gsub und können die Künstlernamen verändern:

def mixedCase(aName)
  aName.gsub(/\b\w/) { $&.upcase }
end
mixedCase("fats waller") » "Fats Waller"
mixedCase("louis armstrong") » "Louis Armstrong"
mixedCase("strength in numbers") » "Strength In Numbers"

Backslashsequenzen in der Ersetzung

Vorher haben wir bemerkt, dass die Sequenzen \1, \2 usw. innerhalb eines Musters verfügbar sind und für die n. Gruppe stehen, die bis jetzt gefunden wurde. Dieselben Sequenzen können auch im zweiten Argument der Methoden sub und gsub angesprochen werden.

"fred:smith".sub(/(\w+):(\w+)/, '\2, \1') » "smith, fred"
"nercpyitno".gsub(/(.)(.)/, '\2\1') » "encryption"

Es gibt einige weiter Backslashsequenzen, die im Ersetzungsstring funktionieren: \& (letzter Fund), \+ (letzte gefundene Gruppe), \` (String vor dem Fund), \' (String nach dem Fund) und \\ (ein Backslash). Es wird verwirrend, wenn Sie versuchen, einen Backslash in einer Ersetzung zu verwenden. Das Offensichtlichste wäre, folgendes zu schreiben:

str.gsub(/\\/, '\\\\')

Es ist klar, dass dieser Code versucht, jeden Backslash in str durch zwei zu ersetzen. Der Programmierer verdoppelte die Backslashes im Ersetzungstext, wobei er wusste, dass sie bei der Syntaxanalyse zu ``\\'' konvertiert werden würden. Wenn die Ersetzung jedoch durchgeführt wird, bearbeitet die Maschine für reguläre Ausdrücke den String ein weiteres Mal und konvertiert ``\\'' zu ``\'', sodass der übrigegebliebene Effekt ist, dass ein einzelner Backslash durch einen anderen einzelnen Backslash ersetzt wird. Sie müssen gsub(/\\/, '\\\\\\\\') schreiben!

str = 'a\b\c' » "a\b\c"
str.gsub(/\\/, '\\\\\\\\') » "a\\b\\c"

Wenn man jedoch die Tatsache verwendet, dass \& durch den gefundenen String ersetzt wird, können Sie das auch so schreiben:

str = 'a\b\c' » "a\b\c"
str.gsub(/\\/, '\&\&') » "a\\b\\c"

Wenn Sie die Blockform von gsub verwenden, wird der String, mit dem ersetzt werden soll, nur ein einziges Mal analysiert, und das Ergebnis ist so, wie Sie es beabsichtigt haben.

str = 'a\b\c' » "a\b\c"
str.gsub(/\\/) { '\\\\' } » "a\\b\\c"

Betrachten Sie schlussendlich folgendes Codefragment aus dem CGI Bibliotheksmodul von Wakou Aoyama als Beispiel für die wunderbare Ausdruckskraft von mit Blöcken kombinierten regulären Ausdrücken. Der Code nimmt einen String mit HTML-Escapesequenzen und konvertiert ihn in normales ASCII. Da es für japanisches Publikum geschrieben wurde, verwendet der Code für den regulären Ausdruck den Modifikator ``n'', der das Parsen von ``wide characters'' (Sonderzeichen) abschaltet. Außerdem zeigt er Rubys case-Ausdruck, den wir ab Seite 83 besprechen.

def unescapeHTML(string)
  str = string.dup
  str.gsub!(/&(.*?);/n) {
    match = $1.dup
    case match
    when /\Aamp\z/ni           then '&'
    when /\Aquot\z/ni          then '"'
    when /\Agt\z/ni            then '>'
    when /\Alt\z/ni            then '<'
    when /\A#(\d+)\z/n         then Integer($1).chr
    when /\A#x([0-9a-f]+)\z/ni then $1.hex.chr
    end
  }
  str
end

puts unescapeHTML("1&lt;2 &amp;&amp; 4&gt;3") puts unescapeHTML("&quot;A&quot; = &#65; = &#x41;")
ergibt:
1<2 && 4>3
"A" = A = A

Objektorientierte reguläre Ausdrücke

Wir müssen zugeben, dass all diese sonderbaren Variablen zwar sehr angenehm zu benutzen, aber nicht sehr objektorientiert sind und außerdem sicherlich kryptisch sind. Und haben wir nicht gesagt, dass alles in Ruby ein Objekt sei? Was ist hier falsch gelaufen?

Nichts, wirklich. Es ist nur so, dass Matz, als er Ruby entwarf, ein vollständig objektorientiertes System für reguläre Ausdrücke erstellte. Dann umhüllte er es mit all diesen $-Variablen, damit sie auch für Perl Programmierer vertraut aussehen. Die Objekte und Klassen sind unterhalb der Oberfläche immer noch da. Graben wir sie nun eine Weile aus.

Wir sind bereits auf eine Klasse gestoßen: ein regulärer Ausdruck erstellt eine Instanz der Klasse Regexp (die ab Seite 366 dokumentiert wird).

re = /cat/
re.type » Regexp

Die Methode Regexp#match findet einen regulären Ausdruck in einem String. Wenn sie erfolglos war, gibt sie nil zurück, andernfalls eine Instanz der Klasse MatchData, deren Dokumentation auf Seite 340 beginnt. Und dieses Objekt von MatchData lässt Ihnen Zugriff auf alle verfügbaren Informationen über einen Treffer. All die feinen Dinge, die Sie über die $-Variablen bekommen können, sind in einem handlichen kleinen Objekt zusammengefasst.

re = /(\d+):(\d+)/     # match a time hh:mm
md = re.match("Time: 12:34am")
md.type » MatchData
md[0]         # == $& » "12:34"
md[1]         # == $1 » "12"
md[2]         # == $2 » "34"
md.pre_match  # == $` » "Time: "
md.post_match # == $' » "am"

Da die Daten eines Treffers in einem eigenen Objekt gespeichert werden, können Sie zur gleichen Zeit die Resultate von zwei oder mehreren Suchen zugreifen, etwas, das Sie mit den $-Variablen nicht tun können. Im nächsten Beispiel suchen wir dasselbe Regexp-Objekt in zwei Strings. Jede Suche gibt ein eigenes MatchData-Objekt zurück. Wir untersuchen die beiden Objekte, indem wir die beiden Musterfelder überprüfen.

re = /(\d+):(\d+)/     # match a time hh:mm
md1 = re.match("Time: 12:34am")
md2 = re.match("Time: 10:30pm")
md1[1, 2] » ["12", "34"]
md2[1, 2] » ["10", "30"]

Wie passen also die $-Variablen hinein? Nach jeder Suche speichert Ruby eine Referenz des Ergebnisses (nil oder ein MatchData-Objekt) in einer für den Thread lokalen Variable (die mit $~ ansprechbar ist). Alle anderen Variablen werden dann von diesem Objekt abgeleitet. Obwohl wir uns nicht wirklich einen Verwendungszweck für den folgenden Code vorstellen können, zeigt er, dass all die anderen MatchData-ähnlichen $-Variablen wirklich vom Wert von $~ abgeleitet werden.

re = /(\d+):(\d+)/
md1 = re.match("Time: 12:34am")
md2 = re.match("Time: 10:30pm")
[ $1, $2 ]   # last successful match » ["10", "30"]
$~ = md1
[ $1, $2 ]   # previous successful match » ["12", "34"]

Nachdem wir all das gesagt haben, müssen wir etwas zugeben. Andy und Dave verwenden normalerweise lieber die $-Variablen, als sich um MatchData-Objekten zu kümmern. Für den tagtäglichen Gebrauch sind sie einfach bequemer. Manchmal können wir's einfach nicht lassen, pragmatisch zu sein.


Extracted from the book "Programming Ruby - The Pragmatic Programmer's Guide"
Übersetzung: Johannes Tanzler
Für das englische Original:
© 2000 Addison Wesley Longman, Inc. Released under the terms of the Open Publication License V1.0. That reference is available for download.
Diese Lizenz sowie das Original vom Herbst 2001 bilden die Grundlage der Übersetzung
Es wird darauf hingewiesen, dass sich die Lizenz des englischen Originals inzwischen geändert hat.
Für die deutsche Übersetzung:
© 2002 Jürgen Katins
Der Copyright-Eigner stellt folgende Lizenzen zur Verfügung:
Nicht-freie Lizenz:
This material may be distributed only subject to the terms and conditions set forth in the Open Publication License, v1.0 or later (the latest version is presently available at http://www.opencontent.org/openpub/). Distribution of substantively modified versions of this document is prohibited without the explicit permission of the copyright holder. Distribution of the work or derivative of the work in any standard (paper) book form is prohibited unless prior permission is obtained from the copyright holder.
Freie Lizenz:
Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.1 or any later version published by the Free Software Foundation; with no Invariant Sections, with no Front-Cover Texts, and with no Back-Cover Texts. A copy of the license is included in the section entitled "GNU Free Documentation License".