Trim leading spaces, then Capitalize first letter of every line, and add new line + string + $var from another file if line begins w specific string

Viewed 74

I have two files. One is structured questions, the other is an answer key.

SourceQuiz.txt sample:

1)This is the first question.
  a)  option 1
  b)  option 2
  c)  option 3
  d)  option 4
2)This is the second question
  a)  option 1
  b)  option 2
  c)  option 3
  d)  option 4
3)This is the third question.
  a)  option 1
  b)  option 2
  c)  option 3
  d)  option 4
4)This is the fourth question.
  a)  option 1
  b)  option 2
  c)  option 3
  d)  option 4 
etc etc

AnswerKey.txt sample:

a
b
d
d

I need FinalQuiz.txt file to be in this format: https://docs.moodle.org/311/en/Aiken_format

What is the correct answer to this question?
A. Is it this one?
B. Maybe this answer?
C. Possibly this one?
D. Must be this one!
ANSWER: D

The fact that the source questions have ) rather than . is immaterial. Moodle interprets them fine.

So I need to:

  1. Strip the number and right parenthesis from the beginning of each question
  2. Trim the leading spaces from the beginning of each question's options
  3. Capitalize the first letter of each line
  4. then look for every line beginning with D and insert a new line with the string ANSWER: x where x is the value in the line corresponding to the question in AnswerKey.txt

So i know i can:

Get-Content $SourceQuiz.txt  | Foreach {$_.TrimEnd()}

and i know i can:

Get-Content C:\Users\Administrator\Desktop\123.txt | ForEach-Object {
  if ($_) {
    $_.Substring(0, 1).ToUpper() + $_.Substring(1)
  } else {
    $_
  }
} > output.txt

And i know i can use if ($_.StarsWIth("D") in a foreach loop to find lines beginning with D.

What I dont know how to do is nest foreach loops in one another to do these either all at once or in a specific order, nor how to add the value of the right line in AnswerKey.txt (in our example above Question 1 would be ANSWER: A, Question 2 would be ANSWER: B, Question 3 would be ANSWER: D, Question 4 would be ANSWER D.

I could probably get this done if i make multiple scripts, but im not sure. ive been banging my head over this for a bit.


Edit for clarification. Here is source once again:

1)This is the first question.
  a)  option 1
  b)  option 2
  c)  option 3
  d)  option 4
2)This is the second question
  a)  option 1
  b)  option 2
  c)  option 3
  d)  option 4
3)This is the third question.
  a)  option 1
  b)  option 2
  c)  option 3
  d)  option 4
4)This is the fourth question.
  a)  option 1
  b)  option 2
  c)  option 3
  d)  option 4 

The result I'm getting:

This is the first question.
a)  option 1
b)  option 2
c)  option 3
d)  option 4
ANSWER: D
This is the second question
a)  option 1
b)  option 2
c)  option 3
d)  option 4
ANSWER: C
This is the third question.
a)  option 1
b)  option 2
c)  option 3
d)  option 4
ANSWER: A
This is the fourth question.
a)  option 1
b)  option 2
c)  option 3
d)  option 4 
ANSWER: B

The result needed:

1)This is the first question.
a)  option 1
b)  option 2
c)  option 3
d)  option 4
ANSWER: D
2)This is the second question
a)  option 1
b)  option 2
c)  option 3
d)  option 4
ANSWER: C
3)This is the third question.
a)  option 1
b)  option 2
c)  option 3
d)  option 4
ANSWER: A
4)This is the fourth question.
a)  option 1
b)  option 2
c)  option 3
d)  option 4 
ANSWER: B

The numbers are missing. These are the indicators the importer looks for to define the end of one question and the beginning of the next.

1 Answers

This is the best I could think of, there is probably a much better way.

Props to mklement0 and this awesome answer where I learnt about script-block-based substitutions :)

NOTE: This code is all assuming the data looks exactly as you have shown in your question.

# In your case, this would be:
# $sourceQuiz = Get-Content path\to\SourceQuiz.txt

$sourceQuiz = @'
1)This is the first question.
  a)  option 1
  b)  option 2
  c)  option 3
  d)  option 4
2)This is the second question
  a)  option 1
  b)  option 2
  c)  option 3
  d)  option 4
3)This is the third question.
  a)  option 1
  b)  option 2
  c)  option 3
  d)  option 4
4)This is the fourth question.
  a)  option 1
  b)  option 2
  c)  option 3
  d)  option 4 
'@ -split '\r?\n'

# And this would be:
# $answers = Get-Content path\to\answers.txt

$answers = @'
a
b
d
d
'@ -split '\r?\n'

$answerIndex = 0
$loopIndex = 1
$txtInfo = (Get-Culture).TextInfo

$sourceQuiz -split '^\d+\)' -ne '' | ForEach-Object {

    [regex]::Replace($_, '\s+(\w+)\)\s+', {
        param($s)
        
        $txtInfo.ToTitleCase($s.Groups[1].Value) + '. '
    })
    
    if($loopIndex % 5 -eq 0)
    {
        'ANSWER: {0}' -f $txtInfo.ToTitleCase($answers[$answerIndex])
        ''
        $answerIndex++
    }
    
    $loopIndex++
}

The above results in:

This is the first question.
A. option 1
B. option 2
C. option 3
D. option 4
ANSWER: A

This is the second question
A. option 1
B. option 2
C. option 3
D. option 4
ANSWER: B

This is the third question.
A. option 1
B. option 2
C. option 3
D. option 4
ANSWER: D

This is the fourth question.
A. option 1
B. option 2
C. option 3
D. option 4 
ANSWER: D

EDIT

Code above was always assuming the number of options would be 4, supposing there would be an unknown number of options, the code would look like below.

Note the usage of -Raw on Get-Content.

# In your case, this would be:
# $sourceQuiz = Get-Content path\to\SourceQuiz.txt -Raw # => -Raw is important here

$sourceQuiz = @'
1)This is the first question.
  a)  option 1
  b)  option 2
  c)  option 3
  d)  option 4
2)This is the second question
  a)  option 1
  b)  option 2
  c)  option 3
  d)  option 4
  e)  option 5
  f)  option 6
3)This is the third question.
  a)  option 1
  b)  option 2
'@

# And this would be:
# $answers = Get-Content path\to\answers.txt # => Here don't use -Raw

$answers = @'
a
b
d
'@ -split '\r?\n'

$answerIndex = 0
$txtInfo = (Get-Culture).TextInfo

$sourceQuiz -split '\d+\)' -ne '' | ForEach-Object {
    
    $lines = $_ -split '\r?\n' -ne ''

    for($i=0;$i -lt $lines.Count;$i++)
    {
        switch($i)
        {
            {$i -eq 0}
            {
                $lines[$i]
                break
            }
            {$i -gt 0}
            {
                [regex]::Replace($lines[$i],'\s+(\w+)\)\s+',{
                    param($s)
        
                    $txtInfo.ToTitleCase($s.Groups[1].Value)+'. '
                })
            }
            {$i -eq $lines.Count-1}
            {
                'ANSWER: {0}' -f $txtInfo.ToTitleCase($answers[$answerIndex])
                ''
            }
        }
    }

    $answerIndex++
}

Now this would look like this:

This is the first question.
A. option 1
B. option 2
C. option 3
D. option 4
ANSWER: A

This is the second question
A. option 1
B. option 2
C. option 3
D. option 4
E. option 5
F. option 6
ANSWER: B

This is the third question.
A. option 1
B. option 2
ANSWER: D

  • Why is -Raw needed for the 2nd code snippet?

Because when reading the file if you use -Raw, the content will be a single multiline string instead of an array of strings string[].

  • Why a single multiline string is required?

Because, since the 2nd code snippet is assuming that the number of possible options will not always be 4, we need to find a way to determine how many options there are.

For this, it takes the content of the file a single string and splits where there are _any amount of digits followed by a ) (i.e. 1), 123), 9999), etc) which separates each question and its corresponding options.

Once we have the questions an it's options divided in different arrays / chunks, we can split each chunk again in carriage returns or new lines and here we can assume that the position 0 of the array will always be the question:

{$i -eq 0}
{
    $lines[$i]
    break
}

The positions greater than 0 of the array will be the options, here is where we trim the leading spaces and replace the character followed by ) for the same character followed by . (i.e.: a) for A.).

{$i -gt 0}
{
    [regex]::Replace($lines[$i],'\s+(\w+)\)\s+',{
        param($s)

        $txtInfo.ToTitleCase($s.Groups[1].Value)+'. '
    })
}

Lastly, to determine when we have reached the end of the available options and to know when to insert the ANSWER:, since I'm using a for loop and using the $i variable as index, on each iteration of the loop the switch is asking if $i is equal to $lines.Count - 1 (if $i has reached the end of the array or last element of the array).

{$i -eq $lines.Count-1}
{
    'ANSWER: {0}' -f $txtInfo.ToTitleCase($answers[$answerIndex])
    ''
}
Related