15.4. Analysering af tekstdata

Ofte er det ikke nok bare at indlæse data, de skal også kunne behandles bagefter.

Det kunne være sjovt at udregne aldersgennemsnit i LaesTekstfil.java. Det kræver, at vi først opdeler data for at finde kolonnen med aldrene og derefter konverterer dem til tal, der kan regnes på.

15.4.1. Opdele inddata (StringTokenizer)

Har man brug for at dele strenge op i mindre dele, kan det gøres med StringTokenizer-klassen, der deler en streng op i bidder (eng.: tokens) efter bestemte skilletegn. Normalt opdeles efter blanktegn, og strengen bliver derfor delt op i ord.

En StringTokenizer oprettes med den streng, der skal opdeles:

  StringTokenizer strengbidder = new StringTokenizer("Hej kære venner!");

Herefter kan man med metoden nextToken() få bidderne frem en efter en.

Metoden hasMoreTokens() er sand, hvis der er flere bidder, og falsk, når vi er nået forbi sidste bid:

  while (strengbidder.hasMoreTokens())
  {
    String bid = strengbidder.nextToken();
    System.out.println("bid: "+bid);
  }
  ...

Resultatet bliver:

  bid: Hej
  bid: kære
  bid: venner!

Ønsker man at opdele efter andet end mellemrum, kan man angive det i StringTokenizer's konstruktør. Herunder opdeler vi en formel efter både "+" og "-". Den sidste parameter angiver, at vi godt vil have skilletegnene, dvs. + og -, ud som selvstændige bidder (i stedet for at de smides væk som ligegyldige):

import java.util.*;
public class LaesFormel
{
  public static void main(String[] args)
  {
    StringTokenizer bidder = new StringTokenizer("2*x*x +8*x -5", "+-", true);
    while (bidder.hasMoreTokens())
    {
      String bid = bidder.nextToken();
      bid = bid.trim();           // fjern mellemrum omkring hver bid

      if (bid.equals("+"))      System.out.print(" plus ");
      else if (bid.equals("-")) System.out.print(" minus ");
      else                      System.out.print("'" + bid + "'");
    }
    System.out.println();
  }
}

Resultatet bliver:

'2*x*x' plus '8*x' minus '5'

Bemærk, at vi kalder trim() på strengene for at fjerne eventuelle blanktegn omkring hver bid.

15.4.2. Konvertere til tal

For at omsætte en streng til et tal (int eller double) skal strengen analyseres (eng.: parse), dvs. undersøges for, om den indeholder et tal, og tallet, som kan være repræsenteret på mange måder, skal findes frem. Det har Integer- og Double-klasserne funktioner til, nemlig hhv. parseInt() og parseDouble().

De tager en streng og returnerer den ønskede type:

  int i = Integer.parseInt("542");
  double d = Double.parseDouble("3.14");

Eksponentiel notation (hvor 9.8E3 betyder 9800) forstås også, og der kan også bruges andre talsystemer end titalsystemet. F.eks. giver Integer.parseInt("00010011",2) tallet 19 (19 svarer til 00010011 i det binære talsystem), og Integer.parseInt("1F",16) giver 31 (1F i det hexadecimale talsystem):

  d = Double.parseDouble("9.8E3");        // d = 9800
  i = Integer.parseInt("00010011",2);      // i = 19
  i = Integer.parseInt("1F",16);          // i = 31

15.4.3. DecimalFormat og DateFormat-klasserne

Klasserne DecimalFormat og DateFormat giver ikke blot mange muligheder for at formatere tal/datoer som strenge, men kan også analysere strenge for forskellige tal- og tidsformater og trække data ud af strenge. De er nærmere beskrevet i slutningen af Kapitel 4.

15.4.4. Samlet eksempel: Statistik

Nu kan vi skrive et statistikprogram. Vi tæller antallet af personer (linjer i filen) og summen af aldrene. Linjerne analyseres og lægges ind i variablerne navn, køn og alder.

import java.io.*;
import java.util.*;
public class LaesTekstfilOgLavStatistik 
{
  public static void main(String[] args)
  {
    int antalPersoner = 0;
    int sumAlder = 0;

    try 
    {
      BufferedReader ind =
        new BufferedReader(new FileReader("skrevet fil.txt"));

      String linje = ind.readLine();
      while (linje != null)
      {
        try 
        {
          StringTokenizer bidder = new StringTokenizer(linje);

          String navn = bidder.nextToken();
          String køn = bidder.nextToken();
          int alder = Integer.parseInt(bidder.nextToken());

          System.out.println(navn+" er "+alder+" år.");
          antalPersoner = antalPersoner + 1;
          sumAlder = sumAlder + alder;
        } catch (Exception u) 
        {
          System.out.println("Fejl. Linjen springes over.");
          u.printStackTrace();
        }
        linje = ind.readLine();
      }

      System.out.println("Aldersgennemsnittet er: "+sumAlder/antalPersoner);
    } catch (FileNotFoundException u) 
    {
      System.out.println("Filen kunne ikke findes.");
    } catch (Exception u) 
    {
      System.out.println("Fejl ved læsning af fil.");
      u.printStackTrace();
    }
  }
}

Resultatet bliver:

person0 er 34 år.
person1 er 26 år.
person2 er 24 år.
person3 er 51 år.
person4 er 16 år.
Aldersgennemsnittet er: 30

Undervejs kan der opstå forskellige undtagelser. Hvis filen ikke eksisterer udskrives "Filen kunne ikke findes", og programmet afslutter. En anden mulig fejl er, at filen er tom. Så vil der opstå en aritmetisk undtagelse, når vi dividerer med antalPersoner, og "Fejl ved læsning af fil" udskrives.

Under analyseringen af linjen kan der også opstå flere forskellige slags undtagelser: Konverteringen til heltal kan gå galt, og der kan være for få bidder, så nextToken() bliver kaldt efter sidste bid.

For eksempel giver linjen "person2 m24" (der mangler et mellemrum mellem m og 24)

Fejl. Linjen springes over.
java.util.NoSuchElementException
        at java.util.StringTokenizer.nextToken(StringTokenizer.java:241)
        at LaesTekstfil.main(LaesTekstfil.java:25)

Hvis disse fejl opstår, fortsætter programmet efter catch-blokken med at læse næste linje af inddata.

Da sumAlder og antalPersoner ændres sidst i try-catch-blokken, vil de kun blive opdateret hvis hele linjen er i orden, og statistikken udregnes derfor kun på grundlag af de gyldige linjer.