JeT
**Context**
Once I spent quite a long time passing all my exams (many MCQ) to `exam` class... Exams are now 100% online... (sigh)
I need now to provide a `csv` where the questions of the MCQ below
```
\documentclass[12pt,addpoints]{exam}
\usepackage[a4paper, margin=1.80cm]{geometry}
\begin{document}
\begin{questions}
\question What is the answer ?
\begin{oneparchoices}
\choice 70
\choice 75
\choice 80
\CorrectChoice 85
\choice None of the above
\end{oneparchoices}
\end{questions}
\end{document}
```
would be diplayed like (**Inc**orrect,**Cor**rect, just to be clear :)
`question,answer1,Cor/Inc,answer2,Cor/Inc,answer3,Cor/Inc ,answer4,Cor/Inc,answer5,Cor/Inc`
And it would render like
`What is the answer ?,Inc,70,Inc,75,Inc,80,Inc,85,Cor,None of the above,Inc`
Each line would obviously be a new question.
No parts or subquestions.
Top Answer
FoggyFinder
Here is a slightly changed version of what I've posted in the comments section:
First, we describe our data types:
```text/x-fsharp
[<RequireQualifiedAccess>]
type QOptionType =
| Correct
| Incorrect
with
override x.ToString() =
match x with
| Correct -> "Cor"
| Incorrect -> "Inc"
type QOption = {
Type : QOptionType
Value : string
}
type Question = {
Title : string
Options : QOption list
}
```
Then, we have to write a function that consumes file's content and produces list of the `Question` for it.
```text/x-fsharp
module private Parser =
open FParsec
let ws = spaces
let str_ws s = pstring s .>> ws
let tillNewline = manyCharsTill anyChar newline .>> ws
let questionTitle = str_ws "\\question" >>. tillNewline |>> string
let pChoiceIncorrect =
str_ws "\\choice" >>. tillNewline
|>> fun s -> { Value = s; Type = QOptionType.Incorrect }
let pChoiceCorrect =
str_ws "\\CorrectChoice" >>. tillNewline
|>> fun s -> { Value = s; Type = QOptionType.Correct }
let choicesParser =
let pOpen = str_ws "\\begin{oneparchoices}"
let pEnd = str_ws "\\end{oneparchoices}"
let p = [ pChoiceCorrect ; pChoiceIncorrect ] |> choice |> many
between pOpen pEnd p
let questionParser =
let pOpen = str_ws "\\begin{questions}"
let pEnd = str_ws "\\end{questions}"
let p =
pipe2 questionTitle choicesParser (fun title options ->
{
Title = title
Options = options
})
|> many
between pOpen pEnd p
let tillDocumentBegin =
str_ws "\\begin{document}"
|> manyCharsTill anyChar
let parser =
tillDocumentBegin >>. questionParser .>> str_ws "\\end{document}" .>> eof
let parse text =
match run parser text with
| ParserResult.Success (result,_,_) ->
Result.Ok result
| ParserResult.Failure (error,_,_) ->
Result.Error error
```
I used the wonderful [FParsec](https://www.quanttec.com/fparsec/) library since it's a fun. You can replace the `Parser` module with something more straightforward (or opposite more complicated).
The next step would be transforming a sequence of questions into a csv file.
```text/x-fsharp
[<RequireQualifiedAccess>]
module CSV =
open CsvHelper
open System.IO
open System.Globalization
let writeData path (data:seq<Question>) =
use sw = new StreamWriter(File.Create path)
use csv = new CsvWriter(sw, CultureInfo.InvariantCulture)
for q in data do
csv.WriteField(q.Title, false)
for opt in q.Options do
csv.WriteField(opt.Value)
csv.WriteField(opt.Type.ToString())
csv.NextRecord()
```
I used the [CsvHelper](https://joshclose.github.io/CsvHelper/getting-started/) library for that purpose. But, again, a simple string can be created manually if you wish.
And finally we have to enumerate all files in some folder, next take only appropriate and, at last, handle them:
```text/x-fsharp
open System.IO
let proceedData folder =
Directory.EnumerateFiles(folder, "*.*", SearchOption.TopDirectoryOnly)
|> Seq.filter (fun path -> Path.GetExtension(path) = ".tex")
|> Seq.iter(fun path ->
match File.ReadAllText path |> Parser.parse with
| Result.Ok qs ->
let name = Path.GetFileNameWithoutExtension(path)
let csvName = name + ".csv"
let csvPath = Path.Combine(Path.GetDirectoryName(path), csvName)
CSV.writeData csvPath qs
printfn "%s is handled" csvPath
| Result.Error error ->
printfn "%s eccor occured:" path
printfn "%s" error)
let sampleFolderPath = "D:\\TestFolder\\TestData"
sampleFolderPath
|> proceedData
```
Link to the [gist](https://gist.github.com/FoggyFinder/fb8444ed0bba870dceaf9fc7aa80c85e) with full code
Conclusion: F# fits perfectly for such tasks :-)
Answer #2
wizzwizz4
Originally on [TopAnswers Python](https://topanswers.xyz/python?q=1540#a1794). This code doesn't really parse TeX; it reads files formatted _exactly_ how you've written your document. It's also very messy.
It's iterator-based, though, meaning you can extend it to implement nearly any “parsing” you like – provided that it can be done on a line-by-line basis, and that you don't need lookahead (e.g. your “exit block” lines can be discarded).
The important thing is that it works. :-)
```python
import csv
from functools import partial
from itertools import takewhile
def questions(lines):
lines = iter(lines)
for line in lines:
if r'\begin{questions}' in line:
break
while True:
for line in lines:
if r'\end{questions}' in line:
return
q = line.split(r'\question', maxsplit=1)
if len(q) == 2:
question = q[1].strip()
break
if r'\begin{oneparchoices}' not in next(lines):
raise ValueError(r"Expected \begin{oneparchoices}")
yield question, tuple(
(answer.strip(), r'Correct' in choice)
for choice, answer in map(
partial(str.split, maxsplit=1),
takewhile(
lambda line: r'\end{oneparchoices}' not in line,
lines
)
)
)
def q_flatten(questions):
for question, answers in questions:
yield (question,) + tuple(a_flatten(answers))
def a_flatten(answers):
for answer, correct in answers:
yield answer
if correct:
yield 'Cor'
else:
yield 'Inc'
with open('MCQ.tex') as in_, \
open('FlattenMCQ.csv', 'w', newline='') as out:
writer = csv.writer(out)
# If you want a header, add it here.
##writer.writerow(("column1", "column2", "etc."))
writer.writerows(q_flatten(questions(in_)))
```