Created
September 26, 2015 23:01
-
-
Save rahulsom/4a7169a65e623a31665b to your computer and use it in GitHub Desktop.
Turn TypeForm data into asciidoctor documents
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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<T> 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<Question<T>> 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<Integer> { | |
@Override | |
String aggregate(List<Question<Integer>> 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<List<String>> { | |
@Override | |
String aggregate(List<Question<List<String>>> 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<String> { | |
@Override | |
String aggregate(List<Question<String>> 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<String> { | |
@Override | |
String aggregate(List<Question<String>> 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<String> { | |
@Override | |
String aggregate(List<Question<String>> answers) { | |
def answers1 = answers. | |
collect { it.answer }. | |
findAll { it } | |
answers1.collect { k -> | |
"=== ${k}\n" | |
}.join('\n') | |
} | |
} | |
@Q | |
class Email extends Question<String> { | |
@Override | |
String aggregate(List<Question<String>> 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<String, Question> 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<String, Question> 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<String, String> | |
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<String, Integer> calcHistogram(List<Double> 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<Integer> 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 "" | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment