2013-05-07 10 views
5

Czytam wiersze z plikuprzepisywania modyfikacje strunowe bardziej funkcjonalne

for (line <- Source.fromFile("test.txt").getLines) { 
    .... 
} 

Zasadniczo chcę, aby uzyskać listę akapitów w końcu. Jeśli linia jest pusta, zaczyna się ona od nowego akapitu i być może warto przetworzyć parę słów kluczowych w przyszłości.

Plik tekstowy zawiera listę wpisów jak ten (lub coś podobnego, jak plik INI)

User=Hans 
Project=Blow up the moon 
The slugs are going to eat the mustard. // multiline possible! 
They are sneaky bastards, those slugs. 

User=.... 

A ja po prostu chce mieć listę [Project] gdzie Project wyglądał

class Project (val User: String, val Name:String, val Desc: String) {} 

Opis jest dużym fragmentem tekstu, który nie rozpoczyna się od <keyword>=, ale może rozciągać się na dowolną liczbę linii.

Wiem, jak to zrobić w stylu iteracyjnym. Po prostu wykonaj listę kontroli słów kluczowych i zapełnij instancję klasy i dodaj ją do listy, aby wrócić później.

Ale myślę, że powinno być możliwe, aby zrobić to w dobrym stylu funkcjonalnym, ewentualnie z match case, yield i rekursji, w wyniku listę obiektów, które mają pól User, Project i tak dalej. Użyta klasa jest znana, podobnie jak wszystkie słowa kluczowe, a format pliku również nie jest ustawiony w kamieniu. W większości staram się nauczyć lepszego stylu funkcjonalnego.

+1

Czy możesz podać przykład pliku test.txt i listę, którą chcesz pobrać? – maks

+0

Podałem prawidłowy przykład jako przykład. :) –

+1

A jaka powinna być zawartość listy wyników? – maks

Odpowiedz

8

Oczywiście coś analizujesz, więc nadszedł czas, aby użyć ... parsera!

Ponieważ Twój język traktuje podziały wiersza jako znaczące, musisz odwołać się do this question, aby poinformować o tym analizator składni.

Oprócz tego dość prosta implementacja byłaby

import scala.util.parsing.combinator.RegexParsers 

case class Project(user: String, name: String, description: String) 

object ProjectParser extends RegexParsers { 
    override val whiteSpace = """[ \t]+""".r 

    def eol : Parser[String] = """\r?\n""".r 

    def user: Parser[String] = "User=" ~> """[^\n]*""".r <~ eol 
    def name: Parser[String] = "Project=" ~> """[^\n]*""".r <~ eol 
    def description: Parser[String] = repsep("""[^\n]+""".r, eol) ^^ { case l => l.mkString("\n") } 
    def project: Parser[Project] = user ~ name ~ description ^^ { case a ~ b ~ c => Project(a, b, c) } 
    def projects: Parser[List[Project]] = repsep(project,eol ~ eol) 
} 

I jak go używać:

val sample = """User=foo1 
Project=bar1 
desc1 
desc2 
desc3 

User=foo 
Project=bar 
desc4 desc5 desc6 
desc7 desc8 desc9""" 

import scala.util.parsing.input._ 
val reader = new CharSequenceReader(sample) 
val res = ProjectParser.parseAll(ProjectParser.projects, reader) 
if(res.successful) { 
    print("Found projects: " + res.get) 
} else { 
    print(res) 
} 
+0

Zaakceptowałem to, ponieważ jest to bardzo łatwe do zrozumienia (z wyjątkiem regex, które są zawsze tajemnicze), chociaż inne oferowane rozwiązania są również całkiem zadbane i nauczyłem się kilku. Dziękuję wam wszystkim. –

-1
class Project (val User: String, val Name:String, val Desc: String) {} 
object Project { 
    def apply(str: String): Project = { 
    val user = somehowFetchUserName(str) 
    val name = somehowFetchProjectName(str) 
    val desc = somehowFetchDescription(str) 
    new Project(user, name, desc) 
    } 
} 

val contents: Array[String] = Source.fromFile("test.txt").mkString.split("\\n\\n") 
val list = contents map(Project(_)) 

zostanie wyświetlona lista projektów.

+0

Niezupełnie dodałem bardziej szczegółowe informacje. –

+0

edytowałem moją odpowiedź – maks

1

Inną możliwą realizację (ponieważ parser jest dość prosty), za pomocą rekurencji:

import scala.io.Source 
case class Project(user: String, name: String, desc: String) 
@scala.annotation.tailrec 
def parse(source: Iterator[String], list: List[Project] = Nil): List[Project] = { 
    val emptyProject = Project("", "", "") 
    @scala.annotation.tailrec 
    def parseProject(project: Option[Project] = None): Option[Project] = { 
    if(source.hasNext) { 
     val line = source.next 
     if(!line.isEmpty) { 
     val splitted = line.span(_ != '=') 
     parseProject(splitted match { 
      case (h, t) if h == "User" => project.orElse(Some(emptyProject)).map(_.copy(user = t.drop(1))) 
      case (h, t) if h == "Project" => project.orElse(Some(emptyProject)).map(_.copy(name = t.drop(1))) 
      case _ => project.orElse(Some(emptyProject)).map(project => project.copy(desc = (if(project.desc.isEmpty) "" else project.desC++ "\n") ++ line)) 
     }) 
     } else project 
    } else project 
    } 

    if(source.hasNext) { 
    parse(source, parseProject().map(_ :: list).getOrElse(list)) 
    } else list.reverse 
} 

Test:

object Test { 
    def source = Source.fromString("""User=Hans 
Project=Blow up the moon 
The slugs are going to eat the mustard. // multiline possible! 
They are sneaky bastards, those slugs. 

User=Plop 
Project=SO 
Some desc""") 

    def test = println(parse(source.getLines)) 
} 

Co daje:

List(Project(Hans,Blow up the moon,The slugs are going to eat the mustard. // multiline possible! 
They are sneaky bastards, those slugs.), Project(Plop,SO,Some desc)) 
1

Aby odpowiedzieć na to pytanie bez również zwalczanie słów kluczowych analizowania, zagiąć linie i kruszywa linie chyba że jest pusta jeden, w którym to przypadku rozpocząć nowy akapit pusty.

lines.foldLeft(List("")) { (l, x) => 
    if (x.isEmpty) "" :: l else (l.head + "\n" + x) :: l.tail 
} reverse 

Zauważysz ten ma kilka zmarszczek w jaki obsługuje on zerowe linie i wielokrotne oraz kończące pustych wierszy. Dostosuj się do swoich potrzeb. Także jeśli chodzi o sprzężenie ciągów, możesz je zebrać w postaci zagnieżdżonej listy i spłaszczyć na końcu (używając .map (_.mkString)), jest to jedynie prezentacja podstawowej techniki składania sekwencji, a nie skalarnej, ale nowej sekwencji.

Spowoduje to utworzenie listy w odwrotnej kolejności, ponieważ lista na początek (: :) jest bardziej wydajna niż dodanie do l w każdym kroku.

1

Oczywiście coś budujesz, więc możesz spróbować ... budowniczego!

Podobnie jak Jürgen, moją pierwszą myślą było spasować, gdzie zbierasz wynik.

A mutable.Builder dokonuje akumulacji w sposób zbalansowany, z kolekcją.generic.CanBuildFrom, aby wskazać konstruktora, który będzie używał do utworzenia kolekcji docelowej ze zbioru źródłowego. Trzymasz to zmienne na tyle długo, aby uzyskać wynik. To jest moja wtyczka do zlokalizowanej zmienności. Nie należy zakładać, że ścieżka od List [String] do List [Project] jest niezmienna.

Do innych delikatnych odpowiedzi (tych z nieujemnymi ocenami aprecjacji) dodałbym, że styl funkcjonalny oznacza rozkład funkcjonalny i zwykle małe funkcje.

Jeśli nie korzystasz z analizatorów składni regex, nie zapomnij o wyrażeń regularnych w dopasowaniach wzorców.

I staraj się oszczędzić kropki. W rzeczywistości wierzę, że jutro jest Dzień Zapasów, a ludzie z wrażliwością na kropki powinni pozostać w domu.

case class Project(user: String, name: String, description: String) 

trait Sample { 
    val sample = """ 
    |User=Hans 
    |Project=Blow up the moon 
    |The slugs are going to eat the mustard. // multiline possible! 
    |They are sneaky bastards, those slugs. 
    | 
    |User=Bob 
    |I haven't thought up a project name yet. 
    | 
    |User=Greta 
    |Project=Burn the witch 
    |It's necessary to escape from the witch before 
    |we blow up the moon. I hope Hans sees it my way. 
    |Once we burn the bitch, I mean witch, we can 
    |wreak whatever havoc pleases us. 
    |""".stripMargin 
} 

object Test extends App with Sample { 
    val kv = "(.*?)=(.*)".r 
    def nonnully(s: String) = if (s == null) "" else s + " " 
    val empty = Project(null, null, null) 
    val (res, dummy) = ((List.empty[Project], empty) /: sample.lines) { (acc, line) => 
    val (sofar, cur) = acc 
    line match { 
     case kv("User", u) => (sofar, cur copy (user = u)) 
     case kv("Project", n) => (sofar, cur copy (name = n)) 
     case kv(k, _)   => sys error s"Bad keyword $k" 
     case x if x.nonEmpty => (sofar, cur copy (description = s"${nonnully(cur.description)}$x")) 
     case _ if cur != empty => (cur :: sofar, empty) 
     case _    => (sofar, empty) 
    } 
    } 
    val ps = if (dummy == empty) res.reverse else (dummy :: res).reverse 
    Console println ps 
} 

Mecz można puree tędy też:

val (res, dummy) = ((List.empty[Project], empty) /: sample.lines) { 
    case ((sofar, cur), kv("User", u))  => (sofar, cur copy (user = u)) 
    case ((sofar, cur), kv("Project", n)) => (sofar, cur copy (name = n)) 
    case ((sofar, cur), kv(k, _))   => sys error s"Bad keyword $k" 
    case ((sofar, cur), x) if x.nonEmpty => (sofar, cur copy (description = s"${nonnully(cur.description)}$x")) 
    case ((sofar, cur), _) if cur != empty => (cur :: sofar, empty) 
    case ((sofar, cur), _)     => (sofar, empty) 
    } 

Przed owczarni, wydawało się prostsze do zrobienia akapity pierwszy. Czy to imperatywne myślenie?

object Test0 extends App with Sample { 
    def grafs(ss: Iterator[String]): List[List[String]] = { 
    val (g, rest) = ss dropWhile (_.isEmpty) span (_.nonEmpty) 
    val others = if (rest.nonEmpty) grafs(rest) else Nil 
    g.toList :: others 
    } 
    def toProject(ss: List[String]): Project = { 
    var p = Project("", "", "") 
    for (line <- ss; parts = line split '=') parts match { 
     case Array("User", u) => p = p.copy(user = u) 
     case Array("Project", n) => p = p.copy(name = n) 
     case Array(k, _)   => sys error s"Bad keyword $k" 
     case Array(text)   => p = p.copy(description = s"${p.description} $text") 
    } 
    p 
    } 
    val ps = grafs(sample.lines) map toProject 
    Console println ps 
}