Create grading-utils package
refs VICE-3564 flag=none test plan: - Specs pass. qa risk: low Change-Id: I108b85fe1ecf54453c07bb0dd0d8963404eeaf3a Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/323067 Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com> QA-Review: Chawn Neal <chawn.neal@instructure.com> Product-Review: Chawn Neal <chawn.neal@instructure.com> Reviewed-by: Drake Harper <drake.harper@instructure.com> Reviewed-by: Jason Gillett <jason.gillett@instructure.com>
This commit is contained in:
parent
6c260d35f2
commit
185d370aa7
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"presets": ["@babel/preset-env"]
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
node_modules
|
||||
lib
|
|
@ -0,0 +1,13 @@
|
|||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## 1.0.0 - 2023-07-19
|
||||
|
||||
### Added
|
||||
|
||||
- A changelog to make changes clear
|
||||
- Initial work on this package
|
|
@ -0,0 +1,23 @@
|
|||
# Grading Utils
|
||||
|
||||
> The Grading Utils extracted in it's own npm package for use across multiple services
|
||||
|
||||
The primary consumer of the `@instructure/grading-utils` is `canvas-lms`, so documentation
|
||||
and references throughout documentation might reflect and assume the use of `canvas-lms`.
|
||||
|
||||
## Install and Setup
|
||||
|
||||
As a published npm module, you can add `@instructure/grading-utils` to your node project by doing
|
||||
the following:
|
||||
|
||||
```bash
|
||||
npm install @instructure/grading-utils --save
|
||||
```
|
||||
|
||||
## Tests
|
||||
|
||||
The test process already builds the assets so you can just run:
|
||||
|
||||
```bash
|
||||
npm run test
|
||||
```
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"name": "@instructure/grading-utils",
|
||||
"version": "1.0.0",
|
||||
"description": "Grading utilities for Instructure apps.",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/index.d.ts",
|
||||
"scripts": {
|
||||
"test": "npm run build && yarn test:jest",
|
||||
"test:jest": "jest --color --runInBand",
|
||||
"build": "tsc"
|
||||
},
|
||||
"author": "Instructure, Inc",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"big.js": "^5.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.0.2",
|
||||
"jest": "^28",
|
||||
"@babel/preset-env": "^7"
|
||||
},
|
||||
"type": "module",
|
||||
"jest": {
|
||||
"verbose": true,
|
||||
"transform": {
|
||||
"^.+\\.jsx?$": "babel-jest"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* Copyright (C) 2023 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// @ts-ignore
|
||||
import Big from 'big.js'
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
export type GradingStandard = [string, number]
|
||||
|
||||
export interface GradingSchemeDataRow {
|
||||
name: string
|
||||
value: number
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use scoreToLetterGrade(score: number, gradingSchemeDataRows: GradingSchemeDataRow[]) instead, which takes
|
||||
* a more reasonably typed object model than the 2d array that this function takes in for gradingScheme data rows.
|
||||
* @param score
|
||||
* @param gradingSchemes
|
||||
*/
|
||||
export function scoreToGrade(score: number, gradingSchemes: GradingStandard[]) {
|
||||
// Because scoreToGrade is being used in a non typescript file, ui/features/grade_summary/jquery/index.js,
|
||||
// score can be NaN despite its type being declared as a number
|
||||
if (typeof score !== 'number' || Number.isNaN(score) || gradingSchemes == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
// convert deprecated 2d array format to newer GradingSchemeDataRow[] format
|
||||
const gradingSchemeDataRows = gradingSchemes.map(row => ({name: row[0], value: row[1]}))
|
||||
return scoreToLetterGrade(score, gradingSchemeDataRows)
|
||||
}
|
||||
|
||||
export function scoreToLetterGrade(score: number, gradingSchemeDataRows: GradingSchemeDataRow[]) {
|
||||
// Because scoreToGrade is being used in a non typescript file, ui/features/grade_summary/jquery/index.js,
|
||||
// score can be NaN despite its type being declared as a number
|
||||
if (typeof score !== 'number' || Number.isNaN(score) || gradingSchemeDataRows == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
const roundedScore = parseFloat(Big(score).round(4))
|
||||
// does the following need .toPrecision(4) ?
|
||||
const scoreWithLowerBound = Math.max(roundedScore, 0)
|
||||
const letter = gradingSchemeDataRows.find((row, i) => {
|
||||
const schemeScore: string = (row.value * 100).toPrecision(4)
|
||||
// The precision of the lower bound (* 100) must be limited to eliminate
|
||||
// floating-point errors.
|
||||
// e.g. 0.545 * 100 returns 54.50000000000001 in JavaScript.
|
||||
return scoreWithLowerBound >= parseFloat(schemeScore) || i === gradingSchemeDataRows.length - 1
|
||||
}) as GradingSchemeDataRow
|
||||
if (!letter) {
|
||||
throw new Error('grading scheme not found')
|
||||
}
|
||||
return letter.name
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* Copyright (C) 2023 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import {describe, test, expect} from '@jest/globals'
|
||||
import {scoreToGrade} from '../lib/index'
|
||||
|
||||
describe('index', () => {
|
||||
describe('scoreToGrade', () => {
|
||||
test('returns null when scheme is null or score is NaN', () => {
|
||||
const gradingScheme = [
|
||||
['A', 0.9],
|
||||
['B', 0.8],
|
||||
['C', 0.7],
|
||||
['D', 0.6],
|
||||
['E', 0.5],
|
||||
]
|
||||
|
||||
expect(scoreToGrade(Number.NaN, gradingScheme)).toBe(null)
|
||||
expect(scoreToGrade(40, null)).toBe(null)
|
||||
expect(scoreToGrade('40', gradingScheme)).toBe(null)
|
||||
expect(scoreToGrade('B', gradingScheme)).toBe(null)
|
||||
})
|
||||
|
||||
test('returns the lowest grade to below-scale scores', () => {
|
||||
const gradingScheme = [
|
||||
['A', 0.9],
|
||||
['B', 0.8],
|
||||
['C', 0.7],
|
||||
['D', 0.6],
|
||||
['E', 0.5],
|
||||
]
|
||||
|
||||
expect(scoreToGrade(40, gradingScheme)).toBe('E')
|
||||
})
|
||||
|
||||
test('accounts for floating-point rounding errors', () => {
|
||||
const gradingScheme = [
|
||||
['A', 0.9],
|
||||
['B+', 0.886],
|
||||
['B', 0.8],
|
||||
['C', 0.695],
|
||||
['D', 0.555],
|
||||
['E', 0.545],
|
||||
['M', 0.0],
|
||||
]
|
||||
|
||||
expect(scoreToGrade(1005, gradingScheme)).toBe('A')
|
||||
expect(scoreToGrade(105, gradingScheme)).toBe('A')
|
||||
expect(scoreToGrade(100, gradingScheme)).toBe('A')
|
||||
expect(scoreToGrade(99, gradingScheme)).toBe('A')
|
||||
expect(scoreToGrade(90, gradingScheme)).toBe('A')
|
||||
expect(scoreToGrade(89.999, gradingScheme)).toBe('B+')
|
||||
expect(scoreToGrade(88.601, gradingScheme)).toBe('B+')
|
||||
expect(scoreToGrade(88.6, gradingScheme)).toBe('B+')
|
||||
expect(scoreToGrade(88.599, gradingScheme)).toBe('B')
|
||||
expect(scoreToGrade(80, gradingScheme)).toBe('B')
|
||||
expect(scoreToGrade(79.999, gradingScheme)).toBe('C')
|
||||
expect(scoreToGrade(79, gradingScheme)).toBe('C')
|
||||
expect(scoreToGrade(69.501, gradingScheme)).toBe('C')
|
||||
expect(scoreToGrade(69.5, gradingScheme)).toBe('C')
|
||||
expect(scoreToGrade(69.499, gradingScheme)).toBe('D')
|
||||
expect(scoreToGrade(60, gradingScheme)).toBe('D')
|
||||
expect(scoreToGrade(55.5, gradingScheme)).toBe('D')
|
||||
expect(scoreToGrade(54.5, gradingScheme)).toBe('E')
|
||||
expect(scoreToGrade(50, gradingScheme)).toBe('M')
|
||||
expect(scoreToGrade(0, gradingScheme)).toBe('M')
|
||||
expect(scoreToGrade(-100, gradingScheme)).toBe('M')
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"allowJs": true, // allows importing of .js files from .ts files
|
||||
"esModuleInterop": true, // more accurately transpiles to the ES6 module spec (maybe not needed w/ babel transpilation)
|
||||
"isolatedModules": true, // required to adhere to babel's single-file transpilation process
|
||||
"jsx": "react", // transpiles jsx to React.createElement calls (maybe not needed w/ babel transpilation)
|
||||
"lib": ["dom", "es2020", "esnext"], // include types for DOM APIs and standard JS up to ES2020
|
||||
"module": "ES2020", // support the most modern ES6-style module syntax
|
||||
"moduleResolution": "node", // required for non-commonjs imports
|
||||
"declaration": true,
|
||||
"emitDeclarationOnly": false,
|
||||
"resolveJsonModule": true, // enables importing json files
|
||||
"skipLibCheck": true, // don't do type-checking on dependencies' internal types for improved performance
|
||||
"strict": true, // enables a bunch of strict mode family options. See https://www.typescriptlang.org/tsconfig#strict
|
||||
"target": "ES2020", // support the most modern JS features (let babel handle transpilation)
|
||||
"noImplicitAny": true,
|
||||
"experimentalDecorators": true,
|
||||
"outDir": "lib"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["**/node_modules", "**/.*/"]
|
||||
}
|
Loading…
Reference in New Issue