Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { FeedbackBox } from './components/Feedback';
import * as hooks from './hooks';
import { ProblemTypeKeys } from '../../../../../data/constants/problem';
import ExpandableTextArea from '../../../../../sharedComponents/ExpandableTextArea';
import { answerRangeFormatRegex } from '../../../data/OLXParser';

const AnswerOption = ({
answer,
Expand Down Expand Up @@ -48,6 +49,11 @@ const AnswerOption = ({
? `${getConfig().STUDIO_BASE_URL}/library_assets/blocks/${blockId}/`
: undefined;

const validateAnswerRange = (value) => {
const cleanedValue = value.replace(/^\s+|\s+$/g, '');
return !cleanedValue.length || answerRangeFormatRegex.test(cleanedValue);
};

const getInputArea = () => {
if ([ProblemTypeKeys.SINGLESELECT, ProblemTypeKeys.MULTISELECT].includes(problemType)) {
return (
Expand Down Expand Up @@ -77,8 +83,9 @@ const AnswerOption = ({
);
}
// Return Answer Range View
const isValidValue = validateAnswerRange(answer.title);
return (
<div>
<Form.Group isInvalid={!isValidValue}>
<Form.Control
as="textarea"
className="answer-option-textarea text-gray-500 small"
Expand All @@ -88,10 +95,15 @@ const AnswerOption = ({
onChange={setAnswerTitle}
placeholder={intl.formatMessage(messages.answerRangeTextboxPlaceholder)}
/>
{!isValidValue && (
<Form.Control.Feedback type="invalid">
<FormattedMessage {...messages.answerRangeErrorText} />
</Form.Control.Feedback>
)}
<div className="pgn__form-switch-helper-text">
<FormattedMessage {...messages.answerRangeHelperText} />
</div>
</div>
</Form.Group>
);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ const messages = defineMessages({
defaultMessage: 'Enter min and max values separated by a comma. Use a bracket to include the number next to it in the range, or a parenthesis to exclude the number. For example, to identify the correct answers as 5, 6, or 7, but not 8, specify [5,8).',
description: 'Helper text describing usage of answer ranges',
},
answerRangeErrorText: {
id: 'authoring.answerwidget.answer.answerRangeErrorText',
defaultMessage: 'Error: Invalid range format. Use brackets or parentheses with values separated by a comma.',
description: 'Error text describing wrong format of answer ranges',
},
});

export default messages;
40 changes: 40 additions & 0 deletions src/editors/containers/ProblemEditor/data/OLXParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,46 @@ export const responseKeys = [
'choicetextresponse',
];

/**
* Regular expression to validate numeric answer ranges in OLX format.
* Matches ranges in the form of (min, max) or [min, max] where:
* - Both min and max are required and can be:
* - integers (e.g. 1, -5)
* - decimals (e.g. 1.5, -0.25)
* - fractions (e.g. 1/2, -3/4)
* - Whitespace around numbers and comma is optional
* - Parentheses () indicate exclusive bounds
* - Square brackets [] indicate inclusive bounds
*
* @example
* // Valid patterns:
* (1, 5)
* [1, 5]
* (1.5, 5.5)
* [-5, 10]
* (-3.5, 7)
* (-1,1)
* (-1,1]
* [-1,1)
* [1,1/2]
* [1/2, 2]
* [1/4, 1/2]
* (1,1/2]
* [1/2, 2)
* (1/4, 1/2)
*
* @example
* // Invalid patterns:
* (5,1)
* (1,)
* (,1)
* [1 5]
* {1,5}
* [--5,10]
* []
*/
export const answerRangeFormatRegex = /^[([]\s*-?(?:\d+(?:\.\d+)?|\d+\/\d+)\s*,\s*-?(?:\d+(?:\.\d+)?|\d+\/\d+)\s*[)\]]$/m;

export const stripNonTextTags = ({ input, tag }) => {
const stripedTags = {};
Object.entries(input).forEach(([key, value]) => {
Expand Down
18 changes: 8 additions & 10 deletions src/editors/containers/ProblemEditor/data/ReactStateOLXParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -407,23 +407,21 @@ class ReactStateOLXParser {
let lowerBoundFraction;
let upperBoundInt;
let upperBoundFraction;
if (rawLowerBound.includes('/')) {
if (rawLowerBound?.includes('/')) {
lowerBoundFraction = rawLowerBound.replace(/[^0-9-/]/gm, '');
const [numerator, denominator] = lowerBoundFraction.split('/');
const lowerBoundFloat = Number(numerator) / Number(denominator);
lowerBoundInt = lowerBoundFloat;
lowerBoundInt = Number(numerator) / Number(denominator);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! What about these fractional values like [1/2,3/4] ? They seem to be supported in the code, and on master they are working, but with this PR it rejects them as invalid.

Screenshot 2025-10-14 at 5 35 51 PM

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, thanks. Also add support for fractions numbers.

} else {
// these regex replaces remove everything that is not a decimal or positive/negative numer
lowerBoundInt = Number(rawLowerBound.replace(/[^0-9-.]/gm, ''));
// these regex replaces remove everything that is not a decimal or positive/negative number
lowerBoundInt = Number(rawLowerBound?.replace(/[^0-9-.]/gm, ''));
}
if (rawUpperBound.includes('/')) {
if (rawUpperBound?.includes('/')) {
upperBoundFraction = rawUpperBound.replace(/[^0-9-/]/gm, '');
const [numerator, denominator] = upperBoundFraction.split('/');
const upperBoundFloat = Number(numerator) / Number(denominator);
upperBoundInt = upperBoundFloat;
upperBoundInt = Number(numerator) / Number(denominator);
} else {
// these regex replaces remove everything that is not a decimal or positive/negative numer
upperBoundInt = Number(rawUpperBound.replace(/[^0-9-.]/gm, ''));
// these regex replaces remove everything that is not a decimal or positive/negative number
upperBoundInt = Number(rawUpperBound?.replace(/[^0-9-.]/gm, ''));
}
if (lowerBoundInt > upperBoundInt) {
const lowerBoundChar = rawUpperBound[rawUpperBound.length - 1] === ']' ? '[' : '(';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ import {
numericInputWithAnswerRange,
textInputWithFeedbackAndHintsWithMultipleAnswers,
numberParseTest,
numericInputWithFractionBounds,
numericInputWithEmptyUpperBound,
numericInputWithSwappedBounds,
numericInputWithMissingLowerBound,
numericInputWithNegativeBounds,
numericInputWithSameBounds,
} from './mockData/editorTestData';
import ReactStateOLXParser from './ReactStateOLXParser';

Expand Down Expand Up @@ -147,4 +153,61 @@ describe('Check React State OLXParser problem', () => {
);
});
});
describe('ReactStateOLXParser numerical response range parsing', () => {
test('handles empty upper bound as same as lower', () => {
const parser = new ReactStateOLXParser({
problem: numericInputWithEmptyUpperBound,
editorObject: numericInputWithEmptyUpperBound,
});
const result = parser.buildNumericalResponse();
expect(result[':@']['@_answer']).toBe('[0,1.5]');
});

test('handles swapped bounds and corrects order', () => {
const parser = new ReactStateOLXParser({
problem: numericInputWithSwappedBounds,
editorObject: numericInputWithSwappedBounds,
});
const result = parser.buildNumericalResponse();
expect(result[':@']['@_answer']).toBe('[2,5]');
});

test('fixes swapped fraction bounds and preserves brackets', () => {
const parser = new ReactStateOLXParser({
problem: numericInputWithFractionBounds,
editorObject: numericInputWithFractionBounds,
});
const result = parser.buildNumericalResponse();
expect(result[':@']['@_answer']).toBe('(1/2,3/2)');
});

test('sets upper bound = lower bound if upper bound missing', () => {
const parser = new ReactStateOLXParser({
problem: numericInputWithMissingLowerBound,
editorObject: numericInputWithMissingLowerBound,
});
const result = parser.buildNumericalResponse();
expect(result[':@']['@_answer']).toBe('[,2.5]');
});

test('handles negative number ranges correctly', () => {
const parser = new ReactStateOLXParser({
problem: numericInputWithNegativeBounds,
editorObject: numericInputWithNegativeBounds,
});

const result = parser.buildNumericalResponse();
expect(result[':@']['@_answer']).toBe('(-5.5,-1)');
});

test('handles same numbers in ranges correctly', () => {
const parser = new ReactStateOLXParser({
problem: numericInputWithSameBounds,
editorObject: numericInputWithSameBounds,
});

const result = parser.buildNumericalResponse();
expect(result[':@']['@_answer']).toBe('[10,10]');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,75 @@ export const numberParseTest = {
hints: [],
question: '<p>What is the content of the register x2 after executing the following three lines of instructions?</p>',
};

export const numericInputWithEmptyUpperBound = {
answers: [
{
id: 'a1',
title: '[1.5,]',
correct: true,
},
],
problemType: 'numericalresponse',
settings: {},
};

export const numericInputWithSwappedBounds = {
answers: [
{
id: 'a1',
title: '[5,2]',
correct: true,
},
],
problemType: 'numericalresponse',
settings: {},
};

export const numericInputWithFractionBounds = {
answers: [
{
id: 'a1',
title: '(3/2,1/2)',
correct: true,
},
],
problemType: 'numericalresponse',
settings: {},
};

export const numericInputWithMissingLowerBound = {
answers: [
{
id: 'a1',
title: '[,2.5]',
correct: true,
},
],
problemType: 'numericalresponse',
settings: {},
};

export const numericInputWithNegativeBounds = {
answers: [
{
id: 'a1',
title: '(-5.5,-1)',
correct: true,
},
],
problemType: 'numericalresponse',
settings: {},
};

export const numericInputWithSameBounds = {
answers: [
{
id: 'a1',
title: '[10,10]',
correct: true,
},
],
problemType: 'numericalresponse',
settings: {},
};