/** * TypeForm.groovy * * Generates asciidoctor output by analyzing a TypeForm survey * * 1. Setup an environment variable TYPEFORM_KEY from the account page on TypeForm. * 2. Run this script with one arg - the code for your survey, e.g. `cC5Ur1` * 3. Use Asciidoctor output in StdOut to do whatever you need to do. */ import groovy.json.JsonSlurper import groovy.time.TimeCategory import groovy.transform.AnnotationCollector import groovy.transform.AutoClone import groovy.transform.AutoCloneStyle import groovy.transform.TupleConstructor import java.text.DecimalFormat import java.text.SimpleDateFormat def TYPEFORM_KEY = System.getenv('TYPEFORM_KEY') def UID = args[0] @TupleConstructor(includeSuperProperties = true) @AutoClone(style = AutoCloneStyle.COPY_CONSTRUCTOR) @AnnotationCollector @interface Q {} @Q abstract class Question implements Serializable { String id, question T answer @Override public String toString() { return "${this.class.simpleName}{" + "id='" + id + '\'' + ", question='" + question + '\'' + ", answer=" + answer + '}'; } abstract String aggregate(List> answers); String mapToTable(Map map) { def keySize = map.keySet().toList()*.toString()*.length().max() def valSize = map.values().toList()*.toString()*.length().max() (['----'] + map.collect { k, v -> "${k.padRight(keySize)} ${v.toString().padLeft(valSize)}" } + ['----']).join('\n') } } @Q class Rating extends Question { @Override String aggregate(List> answers) { def map = answers.collect { it.answer }. groupBy { it }. sort { a, b -> a.key <=> b.key }. collectEntries { k, v -> ['*' * (k.toInteger()), v.size()] } mapToTable map } } @Q class MultipleChoice extends Question> { @Override String aggregate(List>> answers) { def map = answers. collect { it.answer }. flatten(). groupBy { it }. collectEntries { k, v -> [k, v.size()] }. sort { a, b -> b.value <=> a.value } mapToTable map } } @Q class Choice extends Question { @Override String aggregate(List> answers) { def map = answers. collect { it.answer }. groupBy { it }. collectEntries { k, v -> [k, v.size()] }. sort { a, b -> b.value <=> a.value } mapToTable map } } @Q class TextField extends Question { @Override String aggregate(List> answers) { def map = answers. collect { it.answer.toLowerCase() }. groupBy { it }. collectEntries { k, v -> [k, v.size()] }. sort { a, b -> b.value <=> a.value } mapToTable map } } @Q class TextArea extends Question { @Override String aggregate(List> answers) { def answers1 = answers. collect { it.answer }. findAll { it } answers1.collect { k -> "=== ${k}\n" }.join('\n') } } @Q class Email extends Question { @Override String aggregate(List> answers) { def map = answers. collect { it.answer }. findAll { it }. groupBy { it }. collectEntries { k, v -> [k, v.size()] }. sort { a, b -> b.value <=> a.value } mapToTable map } } @Newify([TextArea, TextField, Choice, MultipleChoice, Rating, Email]) Question createQuestion(Map q) { switch (q.id) { case ~/textfield_.*/: return TextField(q.id, q.question) case ~/list_.*_choice/: return Choice(q.id, q.question) case ~/list_.*_choice_.*/: return MultipleChoice((q.id =~ /(list_.*_choice)_.*/)[0][1] as String, q.question) case ~/rating_.*/: return Rating(q.id, q.question) case ~/textarea_.*/: return TextArea(q.id, q.question) case ~/email_.*/: return Email(q.id, q.question) default: return null } } Question findQuestion(Map questions, String input) { def re = /[a-z]+_\d+(_[a-z]+)?/ questions[(input =~ re)[0][0]] } def url = "https://api.typeform.com/v0/form/$UID?key=$TYPEFORM_KEY&completed=true".toURL() def json = new JsonSlurper().parse(url.newReader()) Map questions = json.questions.collect { Map q -> createQuestion(q) }.unique().collectEntries { [it.id, it] } def responses = json.responses.collect { Map response -> def ra = response.answers as Map questions.collect { k, v -> def nv = v.clone() if (v instanceof MultipleChoice) { def answers = ra.findAll { k1, v1 -> k1.startsWith(k) } nv.answer = answers.values().findAll { it } } else { def answers = ra.find { k1, v1 -> k1.startsWith(k) } nv.answer = answers.value } nv } } def tsp = new SimpleDateFormat('yyyy-MM-dd HH:mm:ss') def times = json.responses.collect { Map response -> use(TimeCategory) { tsp.parse(response.metadata.date_submit) - tsp.parse(response.metadata.date_land) }.toMilliseconds() / 1000 } def Map calcHistogram(List data, double min, double max, int numBins) { final int[] result = new int[numBins]; final double binSize = (max - min) / numBins; data.each { double d -> int bin = (int) ((d - min) / binSize); if (bin < 0) { /* this data is smaller than min */ } else if (bin >= numBins) { /* this data point is bigger than max */ } else { result[bin] += 1; } } result def retval = [:] numBins.times { i -> double avgTime = min + (i + 0.5) * binSize def nf = new DecimalFormat('#0') retval["~${nf.format(avgTime)} s"] = result[i] } retval } def hist(List times) { calcHistogram( times.collect { it.doubleValue() }, times.min().doubleValue(), times.max().doubleValue(), Math.ceil(Math.sqrt(times.size())) as int ) } println '== Time to complete survey' println '' println '----' println hist(times).collect { k, v -> "${k.padLeft(10)}: ${'*' * v}" }.join('\n') println '----' println '' questions.each { k, v -> println "== ${v.question}" println "" def answers = responses.collect { it.find { it.id == k } } println answers.find()?.aggregate(answers) println "" }