~technomancy/fennel-lang.org

fennel-lang.org/survey/summary.fnl -rw-r--r-- 6.5 KiB
25a294adPhil Hagelberg Remove the bit about submitting fennelconf 2021 talks. a day ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
;; this file spits out the HTML file summarizing the results using a
;; combination of SVG bar graphs and commentary text. it reads the results
;; written by the save.fnl file in raw HTTP form encoded format.
(local html (require :html))
(local foot (require :foot))
(local questions (require :questions))
(local chart (require :chart))

(local year 2021)

(fn parse [contents] ; for form-encoded data
  (fn decode [str] (str:gsub "%%(%x%x)" (fn [v] (string.char (tonumber v 16)))))
  (let [out {}]
    (each [k v (contents:gmatch "([^&=]+)=([^&=]+)")]
      (let [key (decode (k:gsub "+" " "))]
        (when (not (. out key))
          (tset out key []))
        (table.insert (. out key) (pick-values 1 (decode (v:gsub "+" " "))))))
    out))

(fn get-responses []
  (with-open [ls (assert (io.popen "ls responses"))] ; lol
    (icollect [l (ls:lines)]
      (with-open [f (assert (io.open (.. "responses/" l)))]
        (parse (f:read :*all))))))

(local responses (get-responses))

(fn frequencies [tbl]
  (let [sums {}]
    (each [_ checks (pairs tbl)]
      (each [_ v (pairs checks)]
        (match (. sums v)
          nil (tset sums v 1)
          n (tset sums v (+ n 1)))))
    sums))

;; workaround for not having :into yet; see comment in survey.fnl
(fn prepend [tbl pre]
  (while (. pre 1)
    (table.insert tbl 1 (table.remove pre))))

(fn others-for [iq question]
  (icollect [i response (ipairs responses)]
    (. (icollect [j qr (pairs response)]
         (and (j:find (.. "^" (tostring iq))) (. qr 1))) 1)))

(fn others [questions]
  (collect [i {: question :type t} (ipairs questions)]
    (if (= t :checkbox)
        (match (others-for i question)
          [a &as o] (values question o)))))

(fn normalize-decade [years split-first?]
  (match (-?> (tonumber years) (/ 10) (math.floor))
    (where 0 split-first?) (.. "0" years)
    decade (string.format "%d0-%d9" decade decade)
    _ years))

(fn normalize-rounding [x]
  (match (x:find "^([%d%.]+)")
    (_ _ digits) (-> digits (tonumber) (math.floor) (tostring))
    _ x))

(fn normalize-number [i response]
  (match response
    [r] [(if (= 11 i)
             (normalize-rounding r)
             (normalize-decade r (= 10 i)))]))

(local aliases {:Male :male :M :male :m :male})

(fn normalize-text [i response]
  (match response
    [r] [(or (. aliases r) r)]))

;; turn URLs in parens into [:a {:href "https://..."} "https://..."] tables
(fn link [text]
  (var last 1)
  (let [out []]
    (each [start url (text:gmatch "()(https://[^%)%s]+)")]
      (table.insert out (text:sub last (- start 1)))
      (table.insert out [:a {:href url} url])
      (set last (+ start (length url))))
    (table.insert out (text:sub last (length text)))
    (table.unpack out)))

;; commentary is a weird format; we are trying to avoid allowing raw HTML in the
;; files to avoid HTML injection, so each question gets a text file full of
;; page-break-separated paragraphs, each of which gets turned into its own <p>
;; tag with links turned into <a href>s.
(fn commentary-for [i]
  (match (io.open (string.format "commentary/%s/%s.txt" year i))
    f (table.unpack (icollect [p (string.gmatch (f:read :*all) "([^\f]+)")]
                      (if (p:find "^\n> ")
                          [:p {:class "commentary quote"} (link (p:sub 3))]
                          [:p {:class "commentary"} (link p)])))))

;; almost all the question types will get a bar graph, but sometimes
;; the less quantitative data can't be summarized this way.
(fn html-for [i q]
  [:div {:class :question}
   [(if (= q.type :section) :h2 :h4) {} (if (not= :submit q.type) q.question)]
   (match q.type
     :checkbox (chart.bar i (frequencies
                             (icollect [_ r (pairs responses)]
                               (. r q.question))))
     :number (chart.bar i (frequencies
                           (icollect [_ r (pairs responses)]
                             (normalize-number i (. r q.question))))
                        #(if (and (tonumber $1) (tonumber $2))
                             (< (tonumber $1) (tonumber $2))
                             (< $1 $2)))
     :text (chart.bar i (if (= i 14)
                            [] ;; 14 isn't really one that works as a chart
                            (frequencies
                             (icollect [_ r (pairs responses)]
                               (normalize-text i (. r q.question))))))
     :textarea [:span {}])
   (commentary-for i)])

;; by default we emit the HTML summary but we can also display different
;; forms of summaries of the data to help write the commentary.
(match ...
  "--view" (print ((require :fennel.view) responses))
  "--others" (print ((require :fennel.view) (others questions)))
  ("--single" f) (print ((require :fennel.view) (with-open [f (io.open f)]
                                                  (parse (f:read :*all)))))
  ("--summarize" i) (each [q amt (pairs (frequencies
                                         (icollect [_ r (pairs responses)]
                                           (. r (. questions (tonumber i)
                                                   :question)))))]
                      (print (string.rep "|" amt) q))
  _ (print (html [:html {:lang "en"}
                  [:head {}
                   [:meta {:charset "UTF-8"}]
                   [:link {:rel "stylesheet"
                           :href "https://fennel-lang.org/fennel.css"}]
                   [:link {:rel "stylesheet"
                           :href "https://code.cdn.mozilla.net/fonts/fira.css"}]
                   [:style {}
                    ".commentary { font-size: 90%; margin: 0 0 1em 3em; }"
                    ".quote { border-left: 1px gray solid; padding-left: 1em; }"
                    ".bar:hover { border: 1px black solid; fill: grey; }"]
                   [:title {} "the Fennel programming language survey"]]
                  [:body {}
                   [:h1 {} (.. "The Fennel Survey Results: " year)]
                   [:p {} "Here's what we got! Results are interspersed with"
                    "commentary. Thanks for participating; see you next year!"]
                   [:p {} "Note that all multiple-choice questions allow any number"
                    " of selections and thus totals do not add up to 100%. There were"
                    " a total of " (tostring (length responses)) " responses."]
                   (doto (icollect [i q (ipairs questions)]
                           (html-for i q))
                     ;; workaround for not having :into in 0.10.0
                     (prepend [:div {}]))
                   foot]])))