Programmering i Ruby

Den Pragmatiske Programmerers Veiledning

Forrige < Innhold ^
Neste >

Standard typer



Til nå har vi hatt det moro med å implementere deler av koden til jukeboksen vår. Vi har tittet på Array, Hash og Proc, men har forsømt de andre vanlige typene i Ruby: tall, strenger, rekker og regulære uttrykk. La oss spandere på oss et par sider på disse grunnleggende byggestenene nå.

Tall

Ruby støtter heltall og flyttall. Heltall kan være av hvilken som helst størrelse (opp til en maksimumsgrense som bestemmes av hvor mye ledig minne du har). Heltall innenfor en gitt mengde (som oftest -230 til 230-1 eller -262 til 262-1) holdes internt i binærform som objekter av klassen Fixnum. Heltall utenfor denne mengden lagres som objekter av klassen Bignum (som for tiden er implementert som et sett short heltall med variabel lengde). Ruby tar seg av konverteringen fram og tilbake mellom disse typene automatisk.

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

Heltall skrives med et valgfritt fortegn, en valgfri grunntallindikator (0 for oktalt, 0x for heksidesimalt, eller 0b for binært), fulgt av en streng med sifre utifra grunntallet. Understrekningstegn i sifferstrengen ignoreres.

123456                    # Fixnum
123_456                   # Fixnum (understrekning ignoreres)
-543                      # negativ Fixnum
123_456_789_123_345_789   # Bignum
0xaabb                    # heksadesimal
0377                      # oktal
-0b101_010                # binær (negetert)

Du kan også få tak i heltallsverdien til et ASCII-tegn eller escape-sekvenser ved å sette et spørsmålstegn foran. Kontroll- og metakombinasjoner kan også lages ved å bruke ?\C-x, ?\M-x, og ?\M-\C-x. Control-versjonen av en verdi er det samme som ``value & 0x9f''. Meta-versjonen av en verdi er ``value | 0x80''. Til sist lager sekvensen ?\C-? et ASCII delete-tegn, 0177.

?a                        # tegn kode
?\n                       # kode for en lineskift (0x0a)
?\C-a                     # control a = ?A & 0x9f = 0x01
?\M-a                     # meta setter bit 7
?\M-\C-a                  # meta og control a
?\C-?                     # delete tegn

En numerisk litteral med et desimaltegn og/eller en eksponent blir til et Float-objekt, som tilsvarer den underliggende arkitekturens double datatype. Etter desimaltegnet må det følge et siffer, siden 1.e3 vil forsøke å kalle metoden e3 i klassen Fixnum.

Alle tall er objekter og motta en rekke forskjellige meldinger (komplett liste starter på sidene 290, 313, 323 og 349(??)). Dermed finner du absoluttverdien til et tall ved å skrive aNumber.abs og ikke abs(aNumber), slik som i (for eksempel) C++.

Heltall støtter også en rekke nyttige iteratorer. Vi har alt sett en---7.times i kodeeksempelet på side 47(??). Blant de andre finner vi upto og downto, som itererer opp og ned mellom to heltall, og step, som ligner mer på en tradisjonell for-løkke.

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

Til slutt en advarsel for Perl-brukere. Strenger som inneholder tall blir ikke automatisk konverter til tall når de brukes i uttrykk. Dette en en vanlig felle når man leser tall fra en fil. Følgende kode gjør (antagelig) ikke det som var meningen.

DATA.each do |line|
  vals = line.split    # del linjen og lagr elementene i val
  print vals[0] + vals[1], " "
end

3 4
5 6
7 8

og du får ut ``34 56 78.'' Hva gikk galt?

Problemet er at innput ble lest som strenger, ikke tall. Pluss operatoren slår sammen strenger, så det er det vi får ut. For å fikse dette slik at tallene legges sammen, kan vi bruke String#to_i metoden for å gjøre strengen om til et heltall først.

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

Strenger

I Ruby er strenger kort og godt sekvenser av bytes med åtte bit. Som oftest inneholder de tegn som skan skrives ut, men det er ikke påkrevd; en streng kan også inneholde binær data. Strenger er objekter av klassen String.

Strenger lages ofte ved hjelp av strenglitteraler---sekvenser av tegn mellom skilletegn. Siden binær data er vanskelig å representere inne i kildekode, kan du bruke forskjellige escape-sekvenser i en strenglitteral. Hver enkelt av dem byttes ut med den tilsvarende binære verdien når programmet kompileres. Valget av type skilletegn bestemmer hvor mye som byttes ut under kompilering. I strenger hvor apostrof (') brukes som skilletegn, blir to påfølgende bakvendte skråstreker til en og en enkelt bakvendt skråstrek fulgt av en apostrof blir til bare en apostrof.

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

Strenger med kråketær som skilletegn har en haug med andre escape-sekvenser. Den vanligste er antagelig ``\n'', linjeskift-tegnet. Tabell 18.2 på side 203(??) gir den fullstendige listen. I tillegg kan du subsituere verdien av et hvilket som helst Ruby uttrykk i en streng ved å benytte sekvensen #{ expr }. Hvis uttrykket bare er en global, klasse- eller instansvariabel, kan du droppe klammeparentesene.

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

Det finnes ytterlige tre måter å lage strenglitteraler på: %q, %Q, og ``here dokumenter.''

%q og %Q starter apostrof og kråketær adskilte strenger.

%q/general single-quoted string/ » general single-quoted string
%Q!general double-quoted string! » general double-quoted string
%Q{Seconds/day: #{24*60*60}} » Seconds/day: 86400

Bokstaven som følger etter ``q'' eller ``Q'' er skilletegnet. Hvis det er en åpnende hakeparentes, klammeparentes, vanlig parentes eller mindre-enn-tegn, blir strengen lest til det tilsvarende lukkende tegnet blir funnet. Hvis ikke leses strengen til det samme skilletegnet finnes.

Helt til slutt nevner vi at du også kan lage en string bed å bruke et here dokument.

aString = <<END_OF_STRING
    The body of the string
    is the input lines up to
    one ending with the same
    text that followed the '<<'
END_OF_STRING

Et here dokument består av linjene i kildekoden opp til, men ikke inklusive, den terminerende strengen du spesifiserer etter << tegnene. Normalt må denne terminerende strengen starte i første kolonne, men hvis du legger til et minustegn etter << tegnene, kan du indentere den.

print <<-STRING1, <<-STRING2
   Concat
   STRING1
      enate
      STRING2
produserer:
     Concat
        enate

Manipulering av strenger

String er sannsynligvis den største av Ruby sine innebygde klasser, med over 75 standard metoder. Vi vil ikke gå igjennom alle her; bibliotekreferansen har en fullstendig liste. Men vi vil se på noen vanlige bruksmåter---bruksmåter som sannsynligvis vil dukke opp under den daglige programmeringen.

Tilbake til jukeboksen. Selv om den er ment å være tilknyttet Internet, beholder den også kopier av noen populære sanger på en lokal harddisk. Dette gjør at vi kan underholde kundene våre til tross for at et ekorn spiser i stykker nettverkskoblingen vår eller lignende.

Av historiske grunner (finnes det noen andre typer?), lagres listen av sanger som rader i en flat fil. Hver rad består av navnet på filen som inneholder selve sangen, sangens lengde, artistens navn og tittelen. Alt dette i felter separert av vertikale streker. En fil kunne for eksempel begynne slik:

/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
         :                  :           :                   :

Utifra det vi ser, er det åpenbart at vi vil trenge noen av String-klassens mange metoder for å hente ut og omforme feltene før vi lager Song-objekter utifra dem. Om ikke annet, må vi:

Første oppgaven på dagordenen er å dele hver linje opp i felter, og det vil metoden String#split klare fint for oss. I dette tilfellet gir vi split metoden et regulært uttrykk, /\s*\|\s*/, som deler linjen opp ved hver vertikal strek den finner, eventuelt med et eller flere mellomrom på begge sider. Videre trenger vi å fjerne linjeskifttegnet på slutten av linjen, så vi bruker String#chomp for å fjerne det rett før vi kaller split.

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]
produserer:
Song: Wonderful World--Louis    Armstrong (2:58)

Dessverre var den som opprinnelig lagde filen så estetisk bevisst at han ordnet artistnavnene i kolonner, slik at noen av dem har ekstra mellomrom. Disse vil ikke se bra ut på vår høyteknologiske, super-twist, Day-Glo flatskjerm, så vi bør fjerne disse før vi fortsetter. Det er flere måter å gjøre dette på, men den enkleste er nok String#squeeze , som reduserer rekker av gjentatte tegn ned til bare et tegn. Vi vil benytte oss av squeeze! versjonen som endrer selve strengobjektet.

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]
produserer:
Song: Wonderful World--Louis Armstrong (2:58)

Til sist har vi det lille problemet med formatteringen av tidsangivelsen: filen sier 2:58 og vi ønsker antallet sekunder, 178. Vi kan bruke split metoden nok en gang, og denne gangen dele tidsfeltet opp rundt kolontegnet.

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

Men i stedet vil vi benytte en relatert metode. Metoden String#scan ligner på split i det at den også deler en streng opp i delstrenger basert på et mønster. Men i motsetning til split, gir du scan det mønsteret du ønsker at delstrengene skal oppfylle. I dette tilfellet ønsker vi at et eller flere sifre for både minutt- og sekunddelen. Mønsteret for et eller flere sifre er /\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]
produserer:
Song: Wonderful World--Louis Armstrong (178)

Jukeboksen vår skal ha mulighet for å søke på nøkkelord. Gitt et ord fra tittelen til en sang eller navnet til en artist, og den skal vise alle sanger som passer. Tast inn ``fats,'' og den kan for eksempel svare med sanger av Fats Domino, Fats Navarro, og Fats Waller. Vi vil implementere dette med å lage en indekseringsklasse. Gi den et objekt og noen strenger, og den vil indeksere under hvert ord (på minimum to bokstaver) som forekommer i disse strengene. Dette vil illustrere en håndfull til av mengden metoder i klassen String.

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

Metoden String#scan henter ut elementer fra en streng utifra et regulært uttrykk. I dette tilfellet er det mønsteret ``\w[-\w']+'' som fanger opp enhver bokstav , fulgt av en eller flere av det som spesifiseres i hakeparentesene(bindestrek, en annen bokstav eller en apostrof). Vi vil gå dypere inn på regulære uttrykk på side 56(??). For å få søket til å ignorere om bokstavene er små eller store, gjør vi alle ordene vi henter ut, eller bruker som søkenøkler, om til små bokstaver. Legg merke til utropstegnet på slutten av det første downcase! metodenavnet. Som med squeeze! vi brukte tildigere, angir utropstegnet at selve objektet endres, i dette tilfellet blir store bokstaver om til små. [ Det er en liten feil i dette eksempelet: sangen ``Gone, Gone, Gone'' ville blitt indeksert tre ganger. Kan du tenke ut en løsning? ]

La oss utvide SongList klassen til å indeksere sangene underveis når de legges til og legge til en metode for å finne en sang utifra et ord.

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

Endelig kan vi teste hele sulamitten.

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")
produserer:
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)

Vi kunne fylt 50 sider med bare å titte på alle metodene i String. Men la oss heller ta en titt på en enklere datatype: rekker.

Rekker

Rekker dukker opp over alt i virkeligheten: januar til desember, 0 til 9, rå til godt stekt, linjene 50 til og med 67, og så videre. Hvis Ruby skal hjelpe oss med å modellere virkeligheten, må Ruby naturlig nok også støtte disse rekkene. Ruby går enda lenger og benytter rekker for å implementere tre forskjellige egenskaper: sekvenser, betingelser og intervaller.

Rekker som sekvenser

Den første og kanskje mest naturlige bruksmåten til rekker er å uttrykke en sekvens. Sekvenser har et startpunkt, et sluttpunkt og en måte å regne ut de mellomliggende verdiene i sekvensen. I Ruby lages disse sekvensene med hjelp av ``..'' og ``...'' rekkeoperatorene. Versjonen med to punktum lager en rekke inklusive det spesifiserte sluttpunktet, mens versjonen med tre punktum eksluderer det.

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

I motsetning til tidligere versjoner av Perl, blir ikke rekker representert internt som lister i Ruby: sekvensen 1..100000 lagres som et Range-objekt som inneholder referanser til to Fixnum-objekter. Hvis du trenger å omdanne en rekke til en liste, kan du bruke to_a metoden.

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

Rekker har en mengde metoder som lar deg iterere over dem og sjekke innholdet på forskjellige vis.

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

Til nå har vi sett rekker av tall og strenger. Men, som forventet av et objekt-orientert språk, kan Ruby lage rekker basert på objekter du selv definerer. De eneste begrensningene er at objektene må svare med neste objekt i sekvensen når succ kalles og objektene må kunne sammenlignes med <=>, den generelle sammenligningsoperatoren. Denne operatoren, <=> som gjerne kalles romskipoperatoren, returnerer -1, 0 eller +1 alt etter om det første argumentet er mindre enn, lik eller større enn det andre argumentet.

Her er en enkel klasse som representerer rader med ``#'' tegn, som vi kunne bruke som en tekstbasert løsning når vi tester volumkontrollen til jukeboksen.

class VU

  include Comparable

  attr :volume

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

  def inspect     '#' * @volume   end

  # Støtte av rekker

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

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

Vi kan teste denne ut ved å lage en rekke av VU-objekter.

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

Rekker som betingelser

I tillegg til å modellere sekvenser, kan rekker også brukes som betingelser. Det følgende eksempelet skriver ut linjesett fra standard innput, hvor den første linjen i hvert sett inneholder ordet ``start'' og den siste linjen ordet ``end.''

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

I bakgrunnen holder rekken orden på tilstanden til hver av testene. Vi vil se nærmere på noen eksempler på dette i beskrivelsen av løkker som starter på side 82(??).

Rekker som intervaller

Den siste bruksmåten av den mangesidige rekken er som en intervaltest, det vil si å sjekke om en verdi er inne i det intervallet som rekken representerer. Dette gjøres med === operatoren, også kalt case equality operator.

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

Eksempelet med et case-uttrykk på sde 81(??) viser denne testen i bruk, da den finner en jazzstil utifra et gitt årstall.

Regulære Uttrykk

Når vi var på side 50(??) og lagde en sangliste fra en fil, brukte vi et regulært uttrykk for å finne skilletegnet mellom feltene i innput filen. Vi sa at uttrykket brukt i line.split(/\s*\|\s*/) slår ut på en vertikal strek med eventuelle mellomrom på hver side. Vi vil nå dykke dypere ned i detaljene til regulære uttrykk og vise hvorfor det er slik.

Regulære uttrykk brukes for å gjenfinne mønstre i strenger. Ruby har innebydt støtte som gjør mønstergjenkjenning og -substitusjon enkelt og konsist. I denne delen vil vi gå igjennom alle hovedegenskapene til regulære uttrykk. Noen detaljer vil vi hoppe over: titt på side 205(??) for mer informasjon.

Regulære uttrykk er objekter av klassen Regexp. De kan lages ved å eksplisitt kalle konstruktøren eller ved å skrive litteraler slik som /pattern/ og %r\pattern\.

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

Når du først har et regulært uttrykk, kan du bruke Regexp#match(aString) eller operatorene =~ (positiv match) og !~ (negativ match) for å se om en streng inneholder noe som passer inn i mønsteret. Disse match-operatorene er definert for både String og Regexp. Hvis begge operandene er strenger, blir den høyre omdannet til et regulært uttrykk.

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

Match-operatorene returnerer indeksen i strengen hvor mønsteret ble funnet. De har også noen bivirkninger i at en haug med Ruby variable settes. Den delen av strengen som passet inn i mønsteret ender opp i $&, biten før ender opp i $` og restene av strengen går til $'. Vi kan bruke dette for å skrive en metode, showRE, som illustrerer hvor et mønster slår ut.

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

Videre settes også endel variabler som er globale for alle tråder, nemlig $~ og $1 til og med $9. $~ er et MatchData-objekt (beskrivelse starter på side 336(??)) som inneholder alt du kan ønske å vite om mønstersammenligningen. Variablene $1 og utover holder verdiene til deler av sammenligningen. Vi kommer inn på disse senere. For de av dere som får noia av å se slike Perl-lignende variabelnavn, ikke gå ennå. Vi har gode nyheter på slutten av kapittelet.

Mønstre

Alle regulære uttrykk inneholder et mønster som beskriver det vi ønsker å finne i strengene i sammenligner med.

I et mønster, representerer alle tegn seg selv, unntatt ., |, (, ), [, {, +, \, ^, $, *, og ?.

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

Hvis du ønsker å finne en av disse tegnene bokstavelig, sett en bakvendt skråstrek foran. Dette forklarer deler av det mønsteret vi brukte for å splitte opp datalinjene til sangene, /\s*\|\s*/. \| betyr ``gjenkjenn en vertikal strek.'' Uten den bakvendte skråstreken, ville ``|'' betyd veksling (som vi beskriver senere).

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

En bakvendt skråstrek fulgt av en bokstav eller et siffer brukes til en spesiell mønstergjenkjenningskonstruksjon som vi dekker senere. I tillegg kan regulære uttrykk inneholde uttrykkssubstitusjoner som #{...}.

Ankre

Normalt vil et regulært uttrykk lette etter første og beste make for mønsteret i en strek. Sammenlign /iss/ opp mot strengen ``Mississippi,'' og den vil finne substrengen ``iss'' som starter på posisjon en. Men hva om du vil tvinge et mønster til kun å slå til på begynnelsen eller slutten av en streng?

Mønstrene ^ og $ tilsvarer begynnelsen og slutten på en linje. Disse brukes ofte for å ankerfeste et mønster: for eksempel vil /^option/ kun slå ut hvis ordet ``option'' er på begynnelsen av linjen. Sekvensen \A slår ut på begynnelsen av en streng, og \z og \Z slår ut på slutten av en streng. (Faktisk, så slår \Z ut på slutten av strengen med mindre strengen slutter med en ``\n'', i hvilket tilfelle \Z slår ut på rett før linjeskifttegnet.)

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

På lignende vis slår mønstrene \b og \B ut på grensene mellom ord og ikke-ord, respektivt. Med ord mener vi her bokstaver, tall og understrekningstegn.

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

Tegnklasser

En tegnklasse er et sett med tegn mellom hakeparentes: [ tegn ] slår ut på envher av tegnene mellom hakeparentesene. [aeiou] vil slå ut på en (engelsk) vokal, [,.:;!?] slår ut på tegnsetting og så videre. Betydningen av spesielle tegn i regulære uttrykk---.|()[{+^$*?---er slått av på innsiden av hakeparentesene. Men normal strengsubstitusjon fungerer fremdeles, så \b tilsvarer et backspace-tegn, \n et linjeskift og så videre (se tabell 18.2 på side 203(??)). I tillegg kan du bruke forkortelsene fra tabell 5.1 på side 59(??), slik at \s betyr hvilket som helst tegn som gir luft i teksten og ikke bare mellomromtegnet.

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

Inne i hakeparentesene, representerer sekvensen c1-c2 alle tegnene mellom c1 og c2, inklusive tegnene selv.

Hvis du ønsker å inkludere bokstavene ] og - i en tegnklasse, må de stå i begynnelsen.

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]

Sett inn et ^-tegn rett etter den åpnende hakeparentesen for å negere tegnklassen: [^a-z] slår ut på enhver bokstav som ikke er en liten bokstav.

Noen tegnklasser er så vanlig brukt at Ruby tilbyr forkortelser for dem. Disse forkortelsene finner du i tabell 5.1 på side 59(??)---de kan brukes både inne i hakeparenteser og i selve mønsterkroppen.

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

Character class abbreviations
Sequence As [ ... ] Meaning
\d [0-9] Digit character
\D [^0-9] Nondigit
\s [\s\t\r\n\f] Whitespace character
\S [^\s\t\r\n\f] Nonwhitespace character
\w [A-Za-z0-9_] Word character
\W [^A-Za-z0-9_] Nonword character

Til sist, et punktum (``.'') utenfor hakeparenteser representerer hvilket som helst tegn med unntak av linjeskift (og i multilinje-modus kan det også representere et linjeskifttegn.)

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

Repetisjon

Når vi spesifiserte mønsteret som delte opp linjen med sanginformasjon, /\s*\|\s*/, sa vi at vi ønsket å finne en vertikal strek omringet av vilkårlig mellomromstegn. Vi vet nå at \s tilsvarer et mellomromstegn, så da virker det logisk dersom stjernen betyr ``en vilkårlig mengde''. Det stemmer, stjerne er en av flere modifikatorer som lar deg slå ut på gjentakelser av et mønster.

Hvis r står for den delen av det regulære uttrykket som er rett før, så vil:

r * tilsvare null eller flere gjentagelser av r.
r + tilsvare en eller flere gjentagelser av r.
r ? tilsvare null eller en utgave av r.
r {m,n} tilsvare minst ``m'' og maksimum ``n'' gjentagelser av r.
r {m,} tilsvare minst ``m'' gjentagelser av r.

Disse gjentagelseskonstruksjonene har høy presedens---de knytter seg kun til det regulære uttrykket rett foran i mønsteret. /ab+/ tilsvarer en ``a'' fulgt av en eller flere ``b''-tegn, og ikke flere gjentagelser av strengen ``ab'' . Du bør være forsiktig med *---mønsteret /a*/ vil slå ut på enhver streng; absolutt alle strenger har jo null eller flere ``a''-tegn.

Man sier gjerne at disse mønstrene er grådige, da de til vanlig slår ut på så mye av strengen som mulig. Du kan gjør dem sparsommelige og få dem til å slå ut på minst mulig, ved å legge til et spørsmålstegn på slutten.

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

Veksling

Vi vet at den vertikale streken har spesiell betydning, fordi vi måtte bruke bakvendt skråstrek foran den i vår linjeoppdelingsmønster tidligere. Dette er er fordi en vanlig vertikal strek ``|'' slår ut på enten det regulære uttrykket før eller etter streken.

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

Her er det en potensiell felle, siden ``|'' har veldig lav presedens. Det siste eksempelet over slår ut på ``red ball'' eller ``angry sky'', og ikke ``red ball sky'' eller ``red angry sky''. For å få den til å slå ut på ``red ball sky'' eller ``red angry sky'', må overstyre presedensen ved hjelp av gruppering.

Gruppering

Du kan bruke parenteser for å gruppere termer inne i et regulært uttrykk. Alt inne i gruppen ansees om et enkelt regulært uttrykk.

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>>

Parenteser brukes også for å samle resultater fra mønstergjenkjenning. Ruby teller over åpnende parenteser og for hver av dem lagres resultatet av den delvise gjennkjenningen frem til den lukkende parentesen. Du kan bruke dette delvise resultatet både i resten av mønsteret og i programmet ditt. Inne i mønsteret angir \1 resultatet fra første gruppering, \2 fra andre og så videre. Utenfor mønsteret gjør spesialvariablene $1, $2 og så videre, samme nytte.

"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"

Muligheten for å bruke deler av gjenkjenningen så langt, gjør at du kan enklere lete etter forskjellige typer gjentakelser.

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

Du kan også bruke tilbakereferanser for å gjenkjenne skilletegn.

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

Substitusjon basert på mønstre

Noen ganger er mønstergjenkjenning i en streng alt du behøver. Hvis en venn utfordrer deg til å finne et ord som inneholder bokstavene a, b, c,d og e i den rekkefølgen, kunne du søkt i en ordliste med mønsteret /a.*b.*c.*d.*e/ og funnet ``absconded'' og ``ambuscade.'' Det må da være verdt et eller annet.

Men av og til ønsker du å endre strengen utifra det som mønsteret gjenkjenner. La oss ta steget tilbake til vår fil med sanglisten. Den som skrev filen la inn alle artistnavnene med små bokstaver. Når vi viser dem frem på skjermen til jukeboksen, vil de sett bedre ut med stor forbokstav. Hvordan kan vi gjør den første bokstaven i hvert ord stor? ...

Metodene String#sub og String#gsub ser etter en porsjon av en streng som passer med deres første argument og bytter det ut med det som er i det andre argumentet. String#sub gjør bare en substitusjon, mens String#gsub bytter ut alle forekomster av mønsteret. Begge rutinene returnerer en ny kopi av String-objektet med endringene. Mutatorversjonene String#sub! og String#gsub! endrer den opprinnelige strengen.

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"

Det andre argumentet til begge disse funksjonene kan enten være en streng eller en blokk. Dersom en blokk brukes, vil verdien som den returnerer bli byttet inn i strengen som modifiseres.

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

Vel, dette ser ut som svaret på problemet vårt med artistnavnene. Mønsteret som finner første bokstaven i et ord er \b\w---se for en grense mellom ord fulgt av en bokstav. Kombinert dette med gsub og vi kan enkelt endre artistenes navn.

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"

Backslash-sekvenser i substitusjon

Tidligere merket vi oss at sekvensene \1, \2, og så videre er tilgjengelig i mønsteret, og inneholder det nte resultatet fra en gjenkjent gruppering så langt. Disse sekvensene er også tilgjengelig i det andre argumentet til sub og gsub.

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

Det finnes ytterlige sekvenser som starter med bakvendt skråstrek som fungerer i substitusjonsstrenger: \& (forrige gjenkjente), \+ (forrige gjenkjente gruppe), \` (delen av strengen før treffet), \' (delen av strengen etter treffet) og \\ (en litteral bakvendt skråstrek) Det kan lett bli litt forvirrende hvis du ønsker å inkludere en bakvendt skråstrek i en substitusjon. Den åpenbare løsningen er å skrive:

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

Det er klart og tydelig at denne koden forsøker å bytte ut hver backslash med to. Programmereren skrev inn backslash-tegn da han visste at ville bli omdannet til ``\\'' under den syntaktiske analysen. Men, når selve substitusjonen tar seg, kjører rutinene for regulære uttrykk nok en tur igjennom strengen og gjør ``\\'' om til ``\'', slik at den totale effekten er å bytte ut en enkelt backslash med en annen enkel backslash. Du må i stedet skrive gsub(/\\/, '\\\\\\\\')!

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

Men vi kunne også ha benyttet det faktum at \& blir byttet ut med den delstrengen som ble gjenkjent, og skrevet

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

Hvis du benytter blokk-versjonen av gsub, blir strengen som skal substitueres inn analysert kun en gang (under den syntaktiske analysen) og resultatet blir som forventet.

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

Til slutt, som et eksempel for hvor herlig enkelt det er å uttrykke seg når man bruker regulære uttrykk sammen med kodeblokker, ta en titt på følgende kodebit fra CGI modulen, skrevet av Wakou Aoyama. Koden tar en streng som inneholder HTML escape-sekvenser og omgjør det til normal ASCII. Da dette ble skrevet for et japansk publikum i sinne, bruker det ``n'' modifikatoren i de regulære uttrykkene for å slå av prosessering av wide-characters. Her illustreres også case uttrykket, som vi starter å omtale på side 81(??).

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;")
produserer:
1<2 && 4>3
"A" = A = A

Objekt-orienterte regulære uttrykk

Vi må ærlig innrømme at selv om alle disse merkelige variablene er veldig hendige å bruke, så er de ikke spesielt objekt-orienterte, og heller kryptiske. Sa vi ikke at alt er et objekt i Ruby? Hva gikk galt?

Ingenting har gått galt. Det er bare slik at når Matz designet Ruby, laget han et fullstendig objekt-orientert systemt for å håndtere regulære uttrykk. Dernest gjorde han det slik at det så mer kjent ut for Perl programmerere, ved å tilby alle disse $-variablene. Objektene og klassene er der fremdeles, men i bakgrunnen. Så la oss bruke litt tid på å få dem frem i lyset.

Vi har alt sett en klasse: regulære uttrykk er instanser av klassen Regexp (dokumentasjonen starter på side 361(??)).

We've already come across one class: regular expression literals create instances of class Regexp (documented beginning on page 361). -->

re = /cat/
re.type » Regexp

Metoden Regexp#match forsøker å gjenkjenne et regulært uttrykk i en streng. Hvis den ikke klarer det, returneres nil. Hvis den klarer det, returneres en instans av klassen MatchData, som dokumenteres på side 336(??) og utover. Dette MatchData-objektet gir deg tilgang til all tilgjengelig informasjon om gjenkjenningen. Alt det snadderet du kan få ut av $-variablene er samlet i et lite, hendig objekt.

re = /(\d+):(\d+)/     # gjenkjenn et tidspunkt 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"

Siden resultatene fra sammenligningen lagres i et eget objekt, kan du holde på to eller flere resultater samtidig, noe som ikke er mulig hvis du bare benytter $-variablene. I det neste eksempelet bruker vi det samme Regexp-objektet på to strenger. Hver sammenligning returnerer en et unikt MatchData-objekt, som vi verifiserer at er riktig, ved å titte i feltene for de to submønstrene.

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

Men hvor kommer $-variablene inn i bildet? Vel, etter hver mønstersammenligning, lagrer Ruby en referanse til resultatet (nil eller et MatchData-objekt) i den tråd-lokale variabelen $~. Alle de andre variablene for regulære uttrykk utledes så fra dette objektet. Selv om vi ikke klarer å komme på en fornuftig bruk av den følgende koden, demonstrerer den i det minste alle de andre $-variablene relatert til MatchData-objektet faktisk hentes fra verdien i $~.

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

Etter å ha sagt alt dette, har vi en tilståelse. Andy og Dave bruker til vanlig $-variablene i stedet for å bekymre seg om MatchData-objekter. Til daglig bruk er og blir de den mest beleielige løsningen. Noen ganger kan vi ikke stoppe oss selv fra å være pragmatiske.

( In progress translation to Norwegian by NorwayRUG. $Revision: 1.15 $ )


Forrige < Innhold ^
Neste >

Extracted from the book "Programming Ruby - The Pragmatic Programmer's Guide".
Translation to norwegian by Norway Ruby User Group.
Copyright for the english original authored by David Thomas and Andrew Hunt:
Copyright © 2001 Addison Wesley Longman, Inc.
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/).

(Please note that the license for the original has changed from the above. The above is the license of the original version that was used as a foundation for the translation efforts.)

Copyright for the norwegian translation:
Copyright © 2002 Norway Ruby User Group.
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.