在clojure中使用嵌套地图/矢量结构的惯用和简洁方法

时间:2017-03-02 09:40:59

标签: json clojure

我是clojure的新手,作为学习语言的练习,我正在用clojure重写一个旧的groovy脚本。对于上下文,脚本在JIRA实例中查询时间条目,在json中接收结果并根据响应生成报告。

我意识到有关嵌套结构遍历的问题已经被无限期地问到了,但是我没有找到对此的直接答案,所以我希望通过惯用和简洁的方式得到clojurists的一些帮助。核心问题是通用的,与此特定代码无关。

我想在clojure中重写以下内容:

// GROOVY CODE
// this part is just here for context
def timeFormat = DateTimeFormat.forPattern('yyyy/MM/dd')
def fromDate   = timeFormat.parseDateTime(opts.f)
def toDate     = timeFormat.parseDateTime(opts.t)

def json       = queryJiraForEntries(opts, http)
def timesheets = [:].withDefault { new TimeSheet() }

// this is what I'm hoping to find a better way for
json.issues.each { issue ->
  issue.changelog.histories.each { history ->
    def date = DateTime.parse(history.created)
    if (date < fromDate || date > toDate) return

    def timeItems = history.items.findAll { it.field == 'timespent' }
    if (!timeItems) return

    def consultant = history.author.displayName
    timeItems.each { item ->
      def from = (item.from ?: 0) as Integer
      def to   = (item.to   ?: 0) as Integer

      timesheets[consultant].entries << new TimeEntry(date: date, issueKey: issue.key, secondsSpent: to - from)
    }
  }
}

(可以找到返回的json的样本结构here

请注意,当我们创建结果时间条目时,我们会使用最外层的issue.key,中级的date,以及最里面的fromto嵌套结构的级别。

在groovy中,return循环中的each只存在于最内层。我相信其余代码应该或多或少地自我解释。

所以我试图解决的一般问题是:给定一个深度嵌套的地图和列表结构:

  • 遍历/过滤到结构的特定深度
  • 对该深度级别的数据执行某些操作,并将结果添加到上下文
  • 遍历/过滤结构
  • 对该深度级别的数据执行某些操作,并将结果添加到上下文
  • ...
  • 在某个最终级别,根据上下文中的数据和该级别的可用数据生成结果。

我发现这种类型的遍历与上下文和转换数据是一种越来越常见的模式。

我目前的解决方案比常规解决方案更加冗长,对于我未经训练的阅读式代码眼睛来说,一眼就难以理解。解析日期等细节并不重要。我正在寻找的是一个简洁的clojure模式。

评论中每个请求

编辑1:,这是我当前的代码。我提前道歉并无耻地责备我的全部新手:

;; CLOJURE CODE
(defn valid-time-item? [item]
  (and (= (:field item) "timespent") (:to item) (:from item)))

(defn history->time-items [history]
  (filter valid-time-item? (:items history)))

(defn history-has-time-items? [history]
  (not-empty (history->time-items history)))

(defn history-in-date-range? [opts history]
  (tcore/within? (tcore/interval (:from-date opts) (:to-date opts))
                 (tformat/parse (tformat/formatters :date-time) (:created history))))

(defn valid-history? [opts h]
  (and (history-has-time-items? h) (history-in-date-range? opts h)))

(defn issue->histories-with-key [issue]
  (map #(assoc % :issue-key (:key issue))(get-in issue [:changelog :histories])))

(defn json->histories [opts json]
  (filter #(valid-history? opts %) (flatten (map issue->histories-with-key (:issues json)))))

(defn time-item->time-entry [item date issue-key]
  (let [get-int (fn [k] (Integer/parseInt (get item k 0)))]
    {:date          (tformat/unparse date-formatter date)
     :issue-key     issue-key
     :seconds-spent (- (get-int :to) (get-int :from)) }))

(defn history->time-entries [opts history]
  (let [date       (tformat/parse (tformat/formatters :date-time) (:created history))
        key        (:issue-key history)]
    (map #(time-item->time-entry % date key) (history->time-items history))))

(defn json->time-entries [opts json]
  (flatten (map #(history->time-entries opts %) (json->histories opts json))))

(defn generate-time-report [opts]
  (json->time-entries opts (query-jira->json opts)))
为了简洁,省略了一些脚手架等。上面的入口点是generate-time-report,它返回一组地图。

issue->histories-with-key中,我通过将问题密钥粘贴到每个历史记录映射中来保留issue.key上下文。除了代码的一般结构,这是我发现丑陋和不可扩展的要点之一。此外,我还没有将consultant维度添加到clojure解决方案中。

编辑2:在评论和输入以下答案之后的第二次尝试。这个更短,使用更接近原始代码的结构,并包含原始代码中的consultant部分:

;; CLOJURE CODE - ATTEMPT 2
(defn create-time-entry [item date consultant issue-key]
  (let [get-int #(Integer/parseInt (or (% item) "0"))]
    {:date          (f/unparse date-formatter date)
     :issue-key     issue-key
     :consultant    consultant
     :seconds-spent (- (get-int :to) (get-int :from)) }))

(defn history->time-entries [history issue-key from-date to-date]
  (let [date       (f/parse (f/formatters :date-time) (:created history))
        items      (filter #(= (:field %) "timespent") (:items history))
        consultant (get-in history [:author :displayName])]
    (when (and (t/within? (t/interval from-date to-date) date) (not-empty items))
      (map #(create-time-entry % date consultant issue-key) items))))

(defn issue->time-entries [issue from-date to-date]
  (mapcat #(history->time-entries % (:key issue) from-date to-date)
          (get-in issue [:changelog :histories])))

(defn json->time-entries [json from-date to-date]
  (mapcat #(issue->time-entries % from-date to-date) (:issues json)))

(defn generate-time-report [opts]
  (let [{:keys [from-date to-date]} opts]
    (filter not-empty
            (json->time-entries (query-jira->json opts) from-date to-date))))

2 个答案:

答案 0 :(得分:2)

我认为您的Clojure代码一点都不差。这就是我如何改进它。只是一些变化。

(defn valid-time-item? [item]
  (and (= (:field item) "timespent") (:to item) (:from item)))

(defn history->time-items [history]
  (filter valid-time-item? (:items history)))

(defn history-has-time-items? [history]
  (not-empty (history->time-items history)))

(defn history-in-date-range? [history from-date to-date]
  (tcore/within? (tcore/interval from-date to-date)
                 (tformat/parse (tformat/formatters :date-time) (:created history))))

(defn valid-history? [h from-date to-date]
  (and (history-has-time-items? h) (history-in-date-range? h from-date to-date)))

(defn issue->histories-with-key [issue]
  (map #(assoc % :issue-key (:key issue)) (get-in issue [:changelog :histories])))

(defn time-item->time-entry [item date issue-key]
  (let [get-int (fn [k] (Integer/parseInt (get item k 0)))]
    {:date date
     :issue-key issue-key
     :seconds-spent (- (get-int :to) (get-int :from))}))

(defn history->time-entries [history]
  (map #(time-item->time-entry % (:created history) (:issue-key history)) (history->time-items history)))

(defn json->time-entries [json opts]
  (let [{:keys [from-date to-date]} opts]
    (->> json
         :issues
         (mapcat issue->histories-with-key)
         (filter #(valid-history? % from-date to-date))
         (mapcat #(history->time-entries %)))))

(defn generate-time-report [opts]
  (json->time-entries opts (query-jira->json opts)))

密钥更改

  • 未实施json->time-entries实施。现在很清楚json变成time-entries的方式。例如。 json - &gt;问题 - &gt;历史 - &gt;时间输入
  • 使用mapcat代替(flatten (map ...
  • 之前的结构:from-date:to-date。我认为将from-dateto-date发送到函数中会使函数签名比opts
  • 更具可读性
  • 交换主题json)和opts的位置。将最重要的参数放在第一个位置,除非它的集合函数包含lambda,如mapfilter等。

答案 1 :(得分:1)

两年后,我想针对我自己的问题发布建议的解决方案。

clojure中解决“下降到一定深度,捕获某些上下文,进一步下降,捕获某些上下文”问题的一种方法是理解。

出于理解,您可以执行以下操作:

@Mutation someMutation!: (name: string) => void

基本上完成了原始groovy代码的工作。此方法返回Clojure中惯用的地图集合。

代码使用Java 8和出色的(defn clojure-rewrite [opts http] (let [from-date (local-date-time (:f opts) 0) to-date (local-date-time (:t opts) 23 59) json (query-jira opts http)] (for [issue (-> json :issues) history (-> issue :changelog :histories) :let [date (local-date-time df (:created history))] :when (before? from-date date to-date) item (:items history) :when (= (:field item) "timespent") :let [secs #(Integer/parseInt (or (% item) "0"))]] {:consultant (-> history :author :displayName) :date date :issue-key (:key issue) :seconds-spent (- (secs :to) (secs :from))}))) 库(为简便起见,代码中未包含clojure.java-time),这使日期解析有些不同,但我认为这种模式非常普遍。

我认为这说明可以解决Clojure中的原始(且相当通用)的问题并保持简洁。实际上,比起我最初的目标和希望的普通代码更为简洁。