Разделить строки в clojure при чтении из файла

Я изучаю clojure в школе, и у меня есть экзамен. Я просто работал над несколькими вещами, чтобы удостовериться, что я все понял.

Я пытаюсь читать из строки строки за строкой, и, как и я, я хочу разделить строку всякий раз, когда есть ";".

Вот мой код до сих пор

(defn readFile []
  (map (fn [line] (clojure.string/split line #";"))
  (with-open [rdr (reader "C:/Users/Rohil/Documents/work.txt.txt")]
    (doseq [line (line-seq rdr)]
      (clojure.string/split line #";")
        (println line)))))

Когда я это делаю, я все равно получаю вывод:

"I;Am;A;String;"

Я что-то пропустил?

Ответ 1

Я не уверен, что вам это нужно в школе, но поскольку Гэри уже дал отличный ответ, рассмотрите это как бонус.

Вы можете делать элегантные преобразования на линиях текста с преобразователями. Необходимым ингредиентом является то, что позволяет обрабатывать линии как сводную коллекцию и которая закрывает читателя, когда вы закончите сокращение:

(defn lines-reducible [^BufferedReader rdr]
  (reify clojure.lang.IReduceInit
    (reduce [this f init]
      (try
        (loop [state init]
          (if (reduced? state)
            @state
            (if-let [line (.readLine rdr)]
              (recur (f state line))
              state)))
        (finally
          (.close rdr))))))

Теперь вы можете сделать следующее, учитывая ввод work.txt:

I;am;a;string
Next;line;please

Подсчитайте длину каждого "split"

(require '[clojure.string :as str])
(require '[clojure.java.io :as io])

(into []
      (comp
       (mapcat #(str/split % #";"))
       (map count))
      (lines-reducible (io/reader "/tmp/work.txt")))
;;=> [1 2 1 6 4 4 6]

Суммируйте длину всех "разделов"

(transduce
 (comp
  (mapcat #(str/split % #";"))
  (map count))
 +
 (lines-reducible (io/reader "/tmp/work.txt")))
;;=> 24

Суммируйте длину всех слов, пока мы не найдем слово длиной более 5

(transduce
 (comp
  (mapcat #(str/split % #";"))
  (map count))
 (fn
   ([] 0)
   ([sum] sum)
   ([sum l]
    (if (> l 5)
      (reduced sum)
      (+ sum l))))
 (lines-reducible (io/reader "/tmp/work.txt")))

или с помощью take-while:

(transduce
 (comp
  (mapcat #(str/split % #";"))
  (map count)
  (take-while #(> 5 %)))
 +
 (lines-reducible (io/reader "/tmp/work.txt")))

Подробнее читайте https://tech.grammarly.com/blog/building-etl-pipelines-with-clojure.

Ответ 2

TL; DR охватывают REPL и охватывают неизменность

Ваш вопрос был "что мне не хватает?" и я бы сказал, что вам не хватает одной из лучших функций Clojure, REPL.

Изменить: вы также можете пропустить, что Clojure использует неизменяемые структуры данных, поэтому

рассмотрим этот фрагмент кода:

(doseq [x [1 2 3]]
   (inc x)
   (prn x))

Этот код не печатает "2 3 4"

он печатает "1 2 3", потому что x не является изменяемой переменной.

Во время первой итерации (inc x) вызывается, возвращает 2, и ее отбрасывают, потому что она ничего не передается, тогда (prn x) печатает значение x, которое по-прежнему равно 1.

Теперь рассмотрим этот фрагмент кода:

(doseq [x [1 2 3]] (prn (inc x)))

Во время первой итерации inc передает свое возвращаемое значение в prn, поэтому вы получаете 2

Длинный пример:

Я не хочу ограбить вас от возможности самостоятельно решить проблему, поэтому в качестве примера я использую другую проблему.

Учитывая файл "birds.txt" с данными "1chicken\n 2duck\n 3Larry" вы хотите написать функцию, которая берет файл и возвращает последовательность имен птиц

Позволяет разбить эту проблему на более мелкие куски:

сначала позволяет прочитать файл и разделить его на строки

(slurp "birds.txt") даст нам весь файл строку

clojure.string/split-lines даст нам набор с каждой строкой в ​​виде элемента в коллекции

(clojure.string/split-lines (slurp "birds.txt")) получает нас ["1chicken" "2duck" "3Larry"]

В этот момент мы могли бы отобразить некоторую функцию над этой коллекцией, чтобы вырезать число, подобное (map #(clojure.string/replace % #"\d" "") birds-collection)

или мы могли бы просто переместить этот шаг вверх по конвейеру, когда весь файл является одной строкой.

Теперь, когда у нас есть все наши части, мы можем объединить их в функциональный конвейер, где результат одной части подается в следующую

В Clojure есть приятный макрос, чтобы сделать это более читаемым, макрос ->

Он принимает результат одного вычисления и вводит его в качестве первого аргумента для следующего

поэтому наш конвейер выглядит следующим образом:

(-> "C:/birds.txt"
     slurp
     (clojure.string/replace #"\d" "") 
     clojure.string/split-lines)

последняя заметка о стиле, для функций Clojure, которые вы хотите придерживаться case kebab, поэтому readFile должен быть read-file

Ответ 3

Я бы оставил его простым и закодировал его так:

(ns tst.demo.core
  (:use tupelo.test)
  (:require [tupelo.core :as t]
            [clojure.string :as str] ))
(def text
 "I;am;a;line;
  This;is;another;one
  Followed;by;this;")

(def tmp-file-name "/tmp/lines.txt")

(dotest
  (spit tmp-file-name text) ; write it to a tmp file
  (let [lines       (str/split-lines (slurp tmp-file-name))
        result      (for [line lines]
                      (for [word (str/split line #";")]
                        (str/trim word)))
        result-flat (flatten result)]
(is= result
  [["I" "am" "a" "line"]
   ["This" "is" "another" "one"]
   ["Followed" "by" "this"]])

Обратите внимание, что result является дважды вложенной (2D) матрицей слов. Самый простой способ отменить это - это функция flatten для создания result-flat:

(is= result-flat
  ["I" "am" "a" "line" "This" "is" "another" "one" "Followed" "by" "this"])))

Вы также можете использовать apply concat, как в:

(is= (apply concat result) result-flat)

Если вы хотите избежать создания 2D-матрицы в первую очередь, вы можете использовать generator function (a la Python) через lazy-gen и yield из библиотеки Tupelo:

(dotest
  (spit tmp-file-name text) ; write it to a tmp file
  (let [lines  (str/split-lines (slurp tmp-file-name))
        result (t/lazy-gen
                 (doseq [line lines]
                   (let [words (str/split line #";")]
                     (doseq [word words]
                       (t/yield (str/trim word))))))]

(is= result
  ["I" "am" "a" "line" "This" "is" "another" "one" "Followed" "by" "this"])))

В этом случае lazy-gen создает функцию генератора. Обратите внимание, что for был заменен на doseq, а функция yield помещает каждое слово в выходную ленивую последовательность.