Skip to content

Instantly share code, notes, and snippets.

@rahulsom
Created September 26, 2015 23:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rahulsom/4a7169a65e623a31665b to your computer and use it in GitHub Desktop.
Save rahulsom/4a7169a65e623a31665b to your computer and use it in GitHub Desktop.
Turn TypeForm data into asciidoctor documents
/**
* 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