QuizGenerator 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- QuizGenerator/README.md +5 -0
- QuizGenerator/__init__.py +27 -0
- QuizGenerator/__main__.py +7 -0
- QuizGenerator/canvas/__init__.py +13 -0
- QuizGenerator/canvas/canvas_interface.py +622 -0
- QuizGenerator/canvas/classes.py +235 -0
- QuizGenerator/constants.py +149 -0
- QuizGenerator/contentast.py +1809 -0
- QuizGenerator/generate.py +362 -0
- QuizGenerator/logging.yaml +55 -0
- QuizGenerator/misc.py +480 -0
- QuizGenerator/mixins.py +539 -0
- QuizGenerator/performance.py +202 -0
- QuizGenerator/premade_questions/__init__.py +0 -0
- QuizGenerator/premade_questions/basic.py +103 -0
- QuizGenerator/premade_questions/cst334/__init__.py +1 -0
- QuizGenerator/premade_questions/cst334/languages.py +395 -0
- QuizGenerator/premade_questions/cst334/math_questions.py +297 -0
- QuizGenerator/premade_questions/cst334/memory_questions.py +1398 -0
- QuizGenerator/premade_questions/cst334/ostep13_vsfs.py +572 -0
- QuizGenerator/premade_questions/cst334/persistence_questions.py +396 -0
- QuizGenerator/premade_questions/cst334/process.py +649 -0
- QuizGenerator/premade_questions/cst463/__init__.py +0 -0
- QuizGenerator/premade_questions/cst463/gradient_descent/__init__.py +3 -0
- QuizGenerator/premade_questions/cst463/gradient_descent/gradient_calculation.py +369 -0
- QuizGenerator/premade_questions/cst463/gradient_descent/gradient_descent_questions.py +305 -0
- QuizGenerator/premade_questions/cst463/gradient_descent/loss_calculations.py +650 -0
- QuizGenerator/premade_questions/cst463/gradient_descent/misc.py +73 -0
- QuizGenerator/premade_questions/cst463/math_and_data/__init__.py +2 -0
- QuizGenerator/premade_questions/cst463/math_and_data/matrix_questions.py +631 -0
- QuizGenerator/premade_questions/cst463/math_and_data/vector_questions.py +534 -0
- QuizGenerator/premade_questions/cst463/neural-network-basics/__init__.py +6 -0
- QuizGenerator/premade_questions/cst463/neural-network-basics/neural_network_questions.py +1264 -0
- QuizGenerator/premade_questions/cst463/tensorflow-intro/__init__.py +6 -0
- QuizGenerator/premade_questions/cst463/tensorflow-intro/tensorflow_questions.py +936 -0
- QuizGenerator/qrcode_generator.py +293 -0
- QuizGenerator/question.py +657 -0
- QuizGenerator/quiz.py +468 -0
- QuizGenerator/typst_utils.py +113 -0
- quizgenerator-0.1.0.dist-info/METADATA +263 -0
- quizgenerator-0.1.0.dist-info/RECORD +44 -0
- quizgenerator-0.1.0.dist-info/WHEEL +4 -0
- quizgenerator-0.1.0.dist-info/entry_points.txt +2 -0
- quizgenerator-0.1.0.dist-info/licenses/LICENSE +674 -0
QuizGenerator/misc.py
ADDED
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
#!env python
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import decimal
|
|
5
|
+
import enum
|
|
6
|
+
import itertools
|
|
7
|
+
import logging
|
|
8
|
+
import math
|
|
9
|
+
from typing import List, Dict, Tuple
|
|
10
|
+
|
|
11
|
+
import fractions
|
|
12
|
+
|
|
13
|
+
log = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class OutputFormat(enum.Enum):
|
|
17
|
+
LATEX = enum.auto(),
|
|
18
|
+
CANVAS = enum.auto()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Answer:
|
|
22
|
+
DEFAULT_ROUNDING_DIGITS = 4
|
|
23
|
+
|
|
24
|
+
class AnswerKind(enum.Enum):
|
|
25
|
+
BLANK = "fill_in_multiple_blanks_question"
|
|
26
|
+
MULTIPLE_ANSWER = "multiple_answers_question" # todo: have baffles?
|
|
27
|
+
ESSAY = "essay_question"
|
|
28
|
+
MULTIPLE_DROPDOWN = "multiple_dropdowns_question"
|
|
29
|
+
|
|
30
|
+
class VariableKind(enum.Enum): # todo: use these for generate variations?
|
|
31
|
+
STR = enum.auto()
|
|
32
|
+
INT = enum.auto()
|
|
33
|
+
FLOAT = enum.auto()
|
|
34
|
+
BINARY = enum.auto()
|
|
35
|
+
HEX = enum.auto()
|
|
36
|
+
BINARY_OR_HEX = enum.auto()
|
|
37
|
+
AUTOFLOAT = enum.auto()
|
|
38
|
+
LIST = enum.auto()
|
|
39
|
+
VECTOR = enum.auto()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self, key:str,
|
|
44
|
+
value,
|
|
45
|
+
kind : Answer.AnswerKind = AnswerKind.BLANK,
|
|
46
|
+
variable_kind : Answer.VariableKind = VariableKind.STR,
|
|
47
|
+
display=None,
|
|
48
|
+
length=None,
|
|
49
|
+
correct=True,
|
|
50
|
+
baffles=None,
|
|
51
|
+
pdf_only=False
|
|
52
|
+
):
|
|
53
|
+
self.key = key
|
|
54
|
+
self.value = value
|
|
55
|
+
self.kind = kind
|
|
56
|
+
self.variable_kind = variable_kind
|
|
57
|
+
# For list values in display, show the first option (or join them with /)
|
|
58
|
+
if display is not None:
|
|
59
|
+
self.display = display
|
|
60
|
+
elif isinstance(value, list) and variable_kind == Answer.VariableKind.STR:
|
|
61
|
+
self.display = value[0] if len(value) == 1 else " / ".join(value)
|
|
62
|
+
else:
|
|
63
|
+
self.display = value
|
|
64
|
+
self.length = length # Used for bits and hex to be printed appropriately
|
|
65
|
+
self.correct = correct
|
|
66
|
+
self.baffles = baffles
|
|
67
|
+
self.pdf_only = pdf_only
|
|
68
|
+
|
|
69
|
+
def get_for_canvas(self) -> List[Dict]:
|
|
70
|
+
# If this answer is marked as PDF-only, don't send it to Canvas
|
|
71
|
+
if self.pdf_only:
|
|
72
|
+
return []
|
|
73
|
+
|
|
74
|
+
canvas_answers : List[Dict] = []
|
|
75
|
+
if self.variable_kind == Answer.VariableKind.BINARY:
|
|
76
|
+
canvas_answers = [
|
|
77
|
+
{
|
|
78
|
+
"blank_id": self.key,
|
|
79
|
+
"answer_text": f"{self.value:0{self.length if self.length is not None else 0}b}",
|
|
80
|
+
"answer_weight": 100 if self.correct else 0,
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"blank_id": self.key,
|
|
84
|
+
"answer_text": f"0b{self.value:0{self.length if self.length is not None else 0}b}",
|
|
85
|
+
"answer_weight": 100 if self.correct else 0,
|
|
86
|
+
}
|
|
87
|
+
]
|
|
88
|
+
elif self.variable_kind == Answer.VariableKind.HEX:
|
|
89
|
+
canvas_answers = [
|
|
90
|
+
{
|
|
91
|
+
"blank_id": self.key,
|
|
92
|
+
"answer_text": f"{self.value:0{(self.length // 8) + 1 if self.length is not None else 0}X}",
|
|
93
|
+
"answer_weight": 100 if self.correct else 0,
|
|
94
|
+
},{
|
|
95
|
+
"blank_id": self.key,
|
|
96
|
+
"answer_text": f"0x{self.value:0{(self.length // 8) + 1 if self.length is not None else 0}X}",
|
|
97
|
+
"answer_weight": 100 if self.correct else 0,
|
|
98
|
+
}
|
|
99
|
+
]
|
|
100
|
+
elif self.variable_kind == Answer.VariableKind.BINARY_OR_HEX:
|
|
101
|
+
canvas_answers = [
|
|
102
|
+
{
|
|
103
|
+
"blank_id": self.key,
|
|
104
|
+
"answer_text": f"{self.value:0{self.length if self.length is not None else 0}b}",
|
|
105
|
+
"answer_weight": 100 if self.correct else 0,
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"blank_id": self.key,
|
|
109
|
+
"answer_text": f"0b{self.value:0{self.length if self.length is not None else 0}b}",
|
|
110
|
+
"answer_weight": 100 if self.correct else 0,
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"blank_id": self.key,
|
|
114
|
+
"answer_text": f"{self.value:0{math.ceil(self.length / 8) if self.length is not None else 0}X}",
|
|
115
|
+
"answer_weight": 100 if self.correct else 0,
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"blank_id": self.key,
|
|
119
|
+
"answer_text": f"0x{self.value:0{math.ceil(self.length / 8) if self.length is not None else 0}X}",
|
|
120
|
+
"answer_weight": 100 if self.correct else 0,
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
"blank_id": self.key,
|
|
124
|
+
"answer_text": f"{self.value}",
|
|
125
|
+
"answer_weight": 100 if self.correct else 0,
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
]
|
|
129
|
+
elif self.variable_kind in [
|
|
130
|
+
Answer.VariableKind.AUTOFLOAT,
|
|
131
|
+
Answer.VariableKind.FLOAT,
|
|
132
|
+
Answer.VariableKind.INT
|
|
133
|
+
]:
|
|
134
|
+
# Use the accepted_strings helper with settings that match the original AUTOFLOAT behavior
|
|
135
|
+
answer_strings = self.__class__.accepted_strings(
|
|
136
|
+
self.value,
|
|
137
|
+
allow_integer=True,
|
|
138
|
+
allow_simple_fraction=True,
|
|
139
|
+
max_denominator=3*4*5, # For process questions, these are the numbers of jobs we'd have
|
|
140
|
+
allow_mixed=True,
|
|
141
|
+
include_spaces=False,
|
|
142
|
+
include_fixed_even_if_integer=True
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
canvas_answers = [
|
|
146
|
+
{
|
|
147
|
+
"blank_id": self.key,
|
|
148
|
+
"answer_text": answer_string,
|
|
149
|
+
"answer_weight": 100 if self.correct else 0,
|
|
150
|
+
}
|
|
151
|
+
for answer_string in answer_strings
|
|
152
|
+
]
|
|
153
|
+
|
|
154
|
+
elif self.variable_kind == Answer.VariableKind.VECTOR:
|
|
155
|
+
|
|
156
|
+
# Get all answer variations
|
|
157
|
+
answer_variations = [
|
|
158
|
+
self.__class__.accepted_strings(dimension_value)
|
|
159
|
+
for dimension_value in self.value
|
|
160
|
+
]
|
|
161
|
+
|
|
162
|
+
canvas_answers = []
|
|
163
|
+
for combination in itertools.product(*answer_variations):
|
|
164
|
+
# Add parentheses format for all vectors: (1, 2, 3)
|
|
165
|
+
canvas_answers.append({
|
|
166
|
+
"blank_id" : self.key,
|
|
167
|
+
"answer_weight": 100 if self.correct else 0,
|
|
168
|
+
"answer_text": f"({', '.join(list(combination))})",
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
# Add non-parentheses format only for single-element vectors: 5
|
|
172
|
+
if len(combination) == 1:
|
|
173
|
+
canvas_answers.append(
|
|
174
|
+
{
|
|
175
|
+
"blank_id": self.key,
|
|
176
|
+
"answer_weight": 100 if self.correct else 0,
|
|
177
|
+
"answer_text": f"{', '.join(combination)}",
|
|
178
|
+
}
|
|
179
|
+
)
|
|
180
|
+
return canvas_answers
|
|
181
|
+
|
|
182
|
+
elif self.variable_kind == Answer.VariableKind.LIST:
|
|
183
|
+
canvas_answers = [
|
|
184
|
+
{
|
|
185
|
+
"blank_id": self.key,
|
|
186
|
+
"answer_text": ', '.join(map(str, possible_state)),
|
|
187
|
+
"answer_weight": 100 if self.correct else 0,
|
|
188
|
+
}
|
|
189
|
+
for possible_state in [self.value] #itertools.permutations(self.value)
|
|
190
|
+
]
|
|
191
|
+
else:
|
|
192
|
+
# For string answers, check if value is a list of acceptable alternatives
|
|
193
|
+
if isinstance(self.value, list):
|
|
194
|
+
canvas_answers = [
|
|
195
|
+
{
|
|
196
|
+
"blank_id": self.key,
|
|
197
|
+
"answer_text": str(alt),
|
|
198
|
+
"answer_weight": 100 if self.correct else 0,
|
|
199
|
+
}
|
|
200
|
+
for alt in self.value
|
|
201
|
+
]
|
|
202
|
+
else:
|
|
203
|
+
canvas_answers = [{
|
|
204
|
+
"blank_id": self.key,
|
|
205
|
+
"answer_text": self.value,
|
|
206
|
+
"answer_weight": 100 if self.correct else 0,
|
|
207
|
+
}]
|
|
208
|
+
|
|
209
|
+
if self.baffles is not None:
|
|
210
|
+
for baffle in self.baffles:
|
|
211
|
+
canvas_answers.append({
|
|
212
|
+
"blank_id": self.key,
|
|
213
|
+
"answer_text": baffle,
|
|
214
|
+
"answer_weight": 0,
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
return canvas_answers
|
|
218
|
+
|
|
219
|
+
def get_display_string(self) -> str:
|
|
220
|
+
"""
|
|
221
|
+
Get the formatted display string for this answer (for grading/answer keys).
|
|
222
|
+
|
|
223
|
+
Returns the answer in the most appropriate format for human readability:
|
|
224
|
+
- BINARY_OR_HEX: hex format (0x...)
|
|
225
|
+
- BINARY: binary format (0b...)
|
|
226
|
+
- HEX: hex format (0x...)
|
|
227
|
+
- AUTOFLOAT/FLOAT: rounded to DEFAULT_ROUNDING_DIGITS
|
|
228
|
+
- INT: integer
|
|
229
|
+
- STR/LIST/VECTOR: as-is
|
|
230
|
+
"""
|
|
231
|
+
if self.variable_kind == Answer.VariableKind.BINARY_OR_HEX:
|
|
232
|
+
# For binary_hex answers, show hex format (more compact and readable)
|
|
233
|
+
hex_digits = math.ceil(self.length / 4) if self.length is not None else 0
|
|
234
|
+
return f"0x{self.value:0{hex_digits}X}"
|
|
235
|
+
|
|
236
|
+
elif self.variable_kind == Answer.VariableKind.BINARY:
|
|
237
|
+
# Show binary format
|
|
238
|
+
return f"0b{self.value:0{self.length if self.length is not None else 0}b}"
|
|
239
|
+
|
|
240
|
+
elif self.variable_kind == Answer.VariableKind.HEX:
|
|
241
|
+
# Show hex format
|
|
242
|
+
hex_digits = (self.length // 4) + 1 if self.length is not None else 0
|
|
243
|
+
return f"0x{self.value:0{hex_digits}X}"
|
|
244
|
+
|
|
245
|
+
elif self.variable_kind == Answer.VariableKind.AUTOFLOAT:
|
|
246
|
+
# Round to default precision for readability
|
|
247
|
+
return f"{round(self.value, self.DEFAULT_ROUNDING_DIGITS)}"
|
|
248
|
+
|
|
249
|
+
elif self.variable_kind == Answer.VariableKind.FLOAT:
|
|
250
|
+
# Round to default precision
|
|
251
|
+
if isinstance(self.value, (list, tuple)):
|
|
252
|
+
return f"{round(self.value[0], self.DEFAULT_ROUNDING_DIGITS)}"
|
|
253
|
+
return f"{round(self.value, self.DEFAULT_ROUNDING_DIGITS)}"
|
|
254
|
+
|
|
255
|
+
elif self.variable_kind == Answer.VariableKind.INT:
|
|
256
|
+
return str(int(self.value))
|
|
257
|
+
|
|
258
|
+
elif self.variable_kind == Answer.VariableKind.LIST:
|
|
259
|
+
return ", ".join(str(v) for v in self.value)
|
|
260
|
+
|
|
261
|
+
elif self.variable_kind == Answer.VariableKind.VECTOR:
|
|
262
|
+
# Format as comma-separated rounded values
|
|
263
|
+
return ", ".join(str(round(v, self.DEFAULT_ROUNDING_DIGITS)) for v in self.value)
|
|
264
|
+
|
|
265
|
+
else:
|
|
266
|
+
# Default: use display or value
|
|
267
|
+
return str(self.display if hasattr(self, 'display') else self.value)
|
|
268
|
+
|
|
269
|
+
# Factory methods for common answer types
|
|
270
|
+
@classmethod
|
|
271
|
+
def binary_hex(cls, key: str, value: int, length: int = None, **kwargs) -> 'Answer':
|
|
272
|
+
"""Create an answer that accepts binary or hex format"""
|
|
273
|
+
return cls(
|
|
274
|
+
key=key,
|
|
275
|
+
value=value,
|
|
276
|
+
variable_kind=cls.VariableKind.BINARY_OR_HEX,
|
|
277
|
+
length=length,
|
|
278
|
+
**kwargs
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
@classmethod
|
|
282
|
+
def auto_float(cls, key: str, value: float, **kwargs) -> 'Answer':
|
|
283
|
+
"""Create an answer that accepts multiple float formats (decimal, fraction, mixed)"""
|
|
284
|
+
return cls(
|
|
285
|
+
key=key,
|
|
286
|
+
value=value,
|
|
287
|
+
variable_kind=cls.VariableKind.AUTOFLOAT,
|
|
288
|
+
**kwargs
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
@classmethod
|
|
292
|
+
def integer(cls, key: str, value: int, **kwargs) -> 'Answer':
|
|
293
|
+
"""Create an integer answer"""
|
|
294
|
+
return cls(
|
|
295
|
+
key=key,
|
|
296
|
+
value=value,
|
|
297
|
+
variable_kind=cls.VariableKind.INT,
|
|
298
|
+
**kwargs
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
@classmethod
|
|
302
|
+
def string(cls, key: str, value: str, **kwargs) -> 'Answer':
|
|
303
|
+
"""Create a string answer"""
|
|
304
|
+
return cls(
|
|
305
|
+
key=key,
|
|
306
|
+
value=value,
|
|
307
|
+
variable_kind=cls.VariableKind.STR,
|
|
308
|
+
**kwargs
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
@classmethod
|
|
312
|
+
def binary(cls, key: str, value: int, length: int = None, **kwargs) -> 'Answer':
|
|
313
|
+
"""Create a binary-only answer"""
|
|
314
|
+
return cls(
|
|
315
|
+
key=key,
|
|
316
|
+
value=value,
|
|
317
|
+
variable_kind=cls.VariableKind.BINARY,
|
|
318
|
+
length=length,
|
|
319
|
+
**kwargs
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
@classmethod
|
|
323
|
+
def hex_value(cls, key: str, value: int, length: int = None, **kwargs) -> 'Answer':
|
|
324
|
+
"""Create a hex-only answer"""
|
|
325
|
+
return cls(
|
|
326
|
+
key=key,
|
|
327
|
+
value=value,
|
|
328
|
+
variable_kind=cls.VariableKind.HEX,
|
|
329
|
+
length=length,
|
|
330
|
+
**kwargs
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
@classmethod
|
|
334
|
+
def float_value(cls, key: str, value: Tuple[float], **kwargs) -> 'Answer':
|
|
335
|
+
"""Create a simple float answer (no fraction conversion)"""
|
|
336
|
+
return cls(
|
|
337
|
+
key=key,
|
|
338
|
+
value=value,
|
|
339
|
+
variable_kind=cls.VariableKind.FLOAT,
|
|
340
|
+
**kwargs
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
@classmethod
|
|
344
|
+
def list_value(cls, key: str, value: list, **kwargs) -> 'Answer':
|
|
345
|
+
"""Create a list answer (comma-separated values)"""
|
|
346
|
+
return cls(
|
|
347
|
+
key=key,
|
|
348
|
+
value=value,
|
|
349
|
+
variable_kind=cls.VariableKind.LIST,
|
|
350
|
+
**kwargs
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
@classmethod
|
|
354
|
+
def vector_value(cls, key: str, value: List[float], **kwargs) -> 'Answer':
|
|
355
|
+
"""Create a simple float answer (no fraction conversion)"""
|
|
356
|
+
return cls(
|
|
357
|
+
key=key,
|
|
358
|
+
value=value,
|
|
359
|
+
variable_kind=cls.VariableKind.VECTOR,
|
|
360
|
+
**kwargs
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
@classmethod
|
|
364
|
+
def dropdown(cls, key: str, value: str, baffles: list = None, **kwargs) -> 'Answer':
|
|
365
|
+
"""Create a dropdown answer with wrong answer choices (baffles)"""
|
|
366
|
+
return cls(
|
|
367
|
+
key=key,
|
|
368
|
+
value=value,
|
|
369
|
+
kind=cls.AnswerKind.MULTIPLE_DROPDOWN,
|
|
370
|
+
baffles=baffles,
|
|
371
|
+
**kwargs
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
@classmethod
|
|
375
|
+
def multiple_choice(cls, key: str, value: str, baffles: list = None, **kwargs) -> 'Answer':
|
|
376
|
+
"""Create a multiple choice answer with wrong answer choices (baffles)"""
|
|
377
|
+
return cls(
|
|
378
|
+
key=key,
|
|
379
|
+
value=value,
|
|
380
|
+
kind=cls.AnswerKind.MULTIPLE_ANSWER,
|
|
381
|
+
baffles=baffles,
|
|
382
|
+
**kwargs
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
@classmethod
|
|
386
|
+
def essay(cls, key: str, **kwargs) -> 'Answer':
|
|
387
|
+
"""Create an essay question (no specific correct answer)"""
|
|
388
|
+
return cls(
|
|
389
|
+
key=key,
|
|
390
|
+
value="", # Essays don't have predetermined answers
|
|
391
|
+
kind=cls.AnswerKind.ESSAY,
|
|
392
|
+
**kwargs
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
@staticmethod
|
|
396
|
+
def _to_fraction(x):
|
|
397
|
+
"""Convert int/float/decimal.Decimal/fractions.Fraction/str('a/b' or decimal) to fractions.Fraction exactly."""
|
|
398
|
+
if isinstance(x, fractions.Fraction):
|
|
399
|
+
return x
|
|
400
|
+
if isinstance(x, int):
|
|
401
|
+
return fractions.Fraction(x, 1)
|
|
402
|
+
if isinstance(x, decimal.Decimal):
|
|
403
|
+
# exact conversion of decimal.Decimal to fractions.Fraction
|
|
404
|
+
sign, digits, exp = x.as_tuple()
|
|
405
|
+
n = 0
|
|
406
|
+
for d in digits:
|
|
407
|
+
n = n * 10 + d
|
|
408
|
+
n = -n if sign else n
|
|
409
|
+
if exp >= 0:
|
|
410
|
+
return fractions.Fraction(n * (10 ** exp), 1)
|
|
411
|
+
else:
|
|
412
|
+
return fractions.Fraction(n, 10 ** (-exp))
|
|
413
|
+
if isinstance(x, str):
|
|
414
|
+
s = x.strip()
|
|
415
|
+
if '/' in s:
|
|
416
|
+
a, b = s.split('/', 1)
|
|
417
|
+
return fractions.Fraction(int(a.strip()), int(b.strip()))
|
|
418
|
+
return fractions.Fraction(decimal.Decimal(s))
|
|
419
|
+
# float or other numerics
|
|
420
|
+
return fractions.Fraction(decimal.Decimal(str(x)))
|
|
421
|
+
|
|
422
|
+
@staticmethod
|
|
423
|
+
def accepted_strings(
|
|
424
|
+
value,
|
|
425
|
+
*,
|
|
426
|
+
allow_integer=True, # allow "whole numbers as whole numbers"
|
|
427
|
+
allow_simple_fraction=True, # allow simple a/b when denominator small
|
|
428
|
+
max_denominator=720, # how "simple" the fraction is
|
|
429
|
+
allow_mixed=False, # also allow "1 1/2" for 3/2
|
|
430
|
+
include_spaces=False, # also accept "1 / 2"
|
|
431
|
+
include_fixed_even_if_integer=False # include "1.0000" when value is 1 and fixed_decimals is set
|
|
432
|
+
):
|
|
433
|
+
"""
|
|
434
|
+
Return a sorted list of strings you can paste into Canvas as alternate correct answers.
|
|
435
|
+
"""
|
|
436
|
+
decimal.getcontext().prec = max(34, (Answer.DEFAULT_ROUNDING_DIGITS or 0) + 10)
|
|
437
|
+
f = Answer._to_fraction(value)
|
|
438
|
+
outs = set()
|
|
439
|
+
|
|
440
|
+
# Integer form
|
|
441
|
+
if f.denominator == 1 and allow_integer:
|
|
442
|
+
outs.add(str(f.numerator))
|
|
443
|
+
if include_fixed_even_if_integer:
|
|
444
|
+
q = decimal.Decimal(1).scaleb(-Answer.DEFAULT_ROUNDING_DIGITS) # 1e-<fixed_decimals>
|
|
445
|
+
d = decimal.Decimal(f.numerator).quantize(q, rounding=decimal.ROUND_HALF_UP)
|
|
446
|
+
outs.add(format(d, 'f'))
|
|
447
|
+
|
|
448
|
+
# Fixed-decimal form (exactly N decimals)
|
|
449
|
+
q = decimal.Decimal(1).scaleb(-Answer.DEFAULT_ROUNDING_DIGITS)
|
|
450
|
+
d = (decimal.Decimal(f.numerator) / decimal.Decimal(f.denominator)).quantize(q, rounding=decimal.ROUND_HALF_UP)
|
|
451
|
+
outs.add(format(d, 'f'))
|
|
452
|
+
|
|
453
|
+
# Trimmed decimal (no trailing zeros; up to max_trimmed_decimals)
|
|
454
|
+
if Answer.DEFAULT_ROUNDING_DIGITS:
|
|
455
|
+
q = decimal.Decimal(1).scaleb(-Answer.DEFAULT_ROUNDING_DIGITS)
|
|
456
|
+
d = (decimal.Decimal(f.numerator) / decimal.Decimal(f.denominator)).quantize(q, rounding=decimal.ROUND_HALF_UP)
|
|
457
|
+
s = format(d, 'f').rstrip('0').rstrip('.')
|
|
458
|
+
# ensure we keep leading zero like "0.5"
|
|
459
|
+
if s.startswith('.'):
|
|
460
|
+
s = '0' + s
|
|
461
|
+
if s == '-0': # tidy negative zero
|
|
462
|
+
s = '0'
|
|
463
|
+
outs.add(s)
|
|
464
|
+
|
|
465
|
+
# Simple fraction (reduced, with small denominator)
|
|
466
|
+
if allow_simple_fraction:
|
|
467
|
+
fr = f.limit_denominator(max_denominator)
|
|
468
|
+
if fr == f:
|
|
469
|
+
a, b = fr.numerator, fr.denominator
|
|
470
|
+
outs.add(f"{a}/{b}")
|
|
471
|
+
if include_spaces:
|
|
472
|
+
outs.add(f"{a} / {b}")
|
|
473
|
+
if allow_mixed and b != 1 and abs(a) > b:
|
|
474
|
+
sign = '-' if a < 0 else ''
|
|
475
|
+
A = abs(a)
|
|
476
|
+
whole, rem = divmod(A, b)
|
|
477
|
+
outs.add(f"{sign}{whole} {rem}/{b}")
|
|
478
|
+
|
|
479
|
+
return sorted(outs, key=lambda s: (len(s), s))
|
|
480
|
+
|