fix grading standard comparisons to use BigDecimals not floats

closes CNVS-27894

Includes fixes for:
- GradingStandard (ruby)
- GradeCalculator (js)
- AssignmentGroupGradesComponent (js) for SRGB

test plan:
1. Create a course
2. From course settings, add a grading scheme with D- having a
   lower boundary of 54.5%
3. Assign this grading scheme to the course
4. Create an assignment worth 100 points
5. Import a student
6. Grade this student's assignment at 54.5/100 exactly
7. The student's grade should be D-
8. From the Grades screen also click on Export to get a CSV with
   the grades.
9. The student's grade in the CSV should also be D-

Change-Id: I7b01eaf83ef00cf77a3dbc1566b82f8992f141f2
Reviewed-on: https://gerrit.instructure.com/76037
Tested-by: Jenkins
Reviewed-by: Jeremy Neander <jneander@instructure.com>
QA-Review: KC Naegle <knaegle@instructure.com>
Product-Review: Christi Wruck
This commit is contained in:
Shahbaz Javeed 2016-04-01 13:32:19 -06:00
parent 8fabb21626
commit 2876c583e3
6 changed files with 100 additions and 5 deletions

View File

@ -17,7 +17,7 @@ define [
letterGrade:(->
standard = @get('gradingStandard')
return null unless standard and @get('hasGrade')
percentage = Math.round(parseInt(@get('percent')), round.DEFAULT)
percentage = parseFloat(@get('rawPercent').toPrecision(4))
GradeCalculator.letter_grade standard, percentage
).property('gradingStandard', 'hasGrade')
@ -31,6 +31,13 @@ define [
"#{round(values.score, round.DEFAULT)} / #{round(values.possible, round.DEFAULT)}"
).property('values')
# This method returns the raw percentage, float errors and all e.g. 54.5 / 100 * 100 will return 54.50000000000001
# It's use is in any further calculations so we're not using a pre-rounded number.
rawPercent:(->
values = @get('values')
values.score / values.possible * 100
).property('values')
percent:(->
values = @get('values')
"#{round((values.score / values.possible)*100, round.DEFAULT)}%"

View File

@ -0,0 +1,49 @@
define [
'ember'
'../start_app'
'../../components/assignment_group_grades_component'
'../shared_ajax_fixtures'
], (Ember, startApp, AGGrades, fixtures) ->
{run} = Ember
originalWeightingScheme = null
originalGradingStandard = null
groupScores =
assignment_group_1:
possible: 100
score: 54.5
submission_count: 1
submissions: []
weight: 100
module 'assignment_group_grades_component_letter_grade',
setup: ->
fixtures.create()
App = startApp()
@component = App.AssignmentGroupGradesComponent.create()
@component.reopen
gradingStandard: (->
originalGradingStandard = this._super
[["A", 0.80],["B+", 55.5],["B", 54.5],["C", 0.05],["F", 0.00]]
).property()
weightingScheme: (->
originalWeightingScheme = this._super
"percent"
).property()
run =>
@assignment_group = Ember.copy(fixtures.assignment_groups, true).findBy('id', '1')
@student = Ember.Object.create Ember.copy groupScores
@component.setProperties
student: @student
ag: @assignment_group
teardown: ->
run =>
@component.destroy()
App.destroy()
test 'letterGrade', ->
expected = "C"
equal @component.get('letterGrade'), expected

View File

@ -263,6 +263,8 @@ define [
@letter_grade: (grading_scheme, score) ->
score = 0 if score < 0
letters = _(grading_scheme).filter (row, i) ->
score >= row[1] * 100 || i == (grading_scheme.length - 1)
# Ensure we're limiting the precision of the lower bound * 100 so we don't get float issues
# e.g. 0.545 * 100 gives 54.50000000000001
score >= (row[1] * 100).toPrecision(4) || i == (grading_scheme.length - 1)
letter = letters[0]
letter[0]

View File

@ -78,7 +78,10 @@ class GradingStandard < ActiveRecord::Base
end
def ordered_scheme
@ordered_scheme ||= grading_scheme.to_a.sort_by { |_, percent| -percent }
# Convert to BigDecimal so we don't get weird float behavior: 0.545 * 100 (gives 54.50000000000001 with floats)
@ordered_scheme ||= grading_scheme.to_a.
map { |grade_letter, percent| [grade_letter, BigDecimal.new(percent.to_s)] }.
sort_by { |_, percent| -percent }
end
def place_in_scheme(key_name)
@ -117,7 +120,8 @@ class GradingStandard < ActiveRecord::Base
score = 0 if score < 0
# assign the highest grade whose min cutoff is less than the score
# if score is less than all scheme cutoffs, assign the lowest grade
ordered_scheme.max_by {|grade_name, lower_bound| score >= lower_bound * 100 ? lower_bound : -lower_bound }[0]
score = BigDecimal.new(score.to_s) # Cast this to a BigDecimal too or comparisons get wonky
ordered_scheme.max_by {|_, lower_bound| score >= lower_bound * 100 ? lower_bound : -lower_bound }[0]
end
def data=(new_val)

View File

@ -298,3 +298,34 @@ define ['compiled/grade_calculator', 'underscore'], (GradeCalculator, _) ->
assertGrade result, 'current', 50, 50
assertGrade result, 'final', 75, 100
test "letter grades are free of float rounding errors", ->
# This spec is as close to identical to the GradeCalculator ruby specs to ensure they both do the same thing
grading_scheme = [['A', 0.90], ['B+', 0.886], ['B', 0.80], ['C', 0.695], ['D', 0.555], ['E', 0.545], ['M', 0.00]]
equal(GradeCalculator.letter_grade(grading_scheme, 1005), 'A')
equal(GradeCalculator.letter_grade(grading_scheme, 105), 'A')
equal(GradeCalculator.letter_grade(grading_scheme, 100), 'A')
equal(GradeCalculator.letter_grade(grading_scheme, 99), 'A')
equal(GradeCalculator.letter_grade(grading_scheme, 90), 'A')
equal(GradeCalculator.letter_grade(grading_scheme, 89.999), 'B+')
equal(GradeCalculator.letter_grade(grading_scheme, 88.601), 'B+')
equal(GradeCalculator.letter_grade(grading_scheme, 88.6), 'B+')
equal(GradeCalculator.letter_grade(grading_scheme, 88.599), 'B')
equal(GradeCalculator.letter_grade(grading_scheme, 80), 'B')
equal(GradeCalculator.letter_grade(grading_scheme, 79.999), 'C')
equal(GradeCalculator.letter_grade(grading_scheme, 79), 'C')
equal(GradeCalculator.letter_grade(grading_scheme, 69.501), 'C')
equal(GradeCalculator.letter_grade(grading_scheme, 69.5), 'C')
equal(GradeCalculator.letter_grade(grading_scheme, 69.499), 'D')
equal(GradeCalculator.letter_grade(grading_scheme, 60), 'D')
equal(GradeCalculator.letter_grade(grading_scheme, 55.5), 'D')
equal(GradeCalculator.letter_grade(grading_scheme, 54.5), 'E')
equal(GradeCalculator.letter_grade(grading_scheme, 50), 'M')
equal(GradeCalculator.letter_grade(grading_scheme, 0), 'M')
equal(GradeCalculator.letter_grade(grading_scheme, -100), 'M')
test "letter grades return the lowest grade to below-scale scores", ->
grading_scheme = [['A', 0.90], ['B', 0.80], ['C', 0.70], ['D', 0.60], ['E', 0.50]]
equal(GradeCalculator.letter_grade(grading_scheme, 40), 'E')

View File

@ -123,7 +123,7 @@ describe GradingStandard do
context "score_to_grade" do
it "should compute correct grades" do
input = [['A', 0.90], ['B+', 0.886], ['B', 0.80], ['C', 0.695], ['D', 0.55], ['M', 0.00]]
input = [['A', 0.90], ['B+', 0.886], ['B', 0.80], ['C', 0.695], ['D', 0.555], ['E', 0.545], ['M', 0.00]]
standard = GradingStandard.new
standard.data = input
expect(standard.score_to_grade(1005)).to eql("A")
@ -142,6 +142,8 @@ describe GradingStandard do
expect(standard.score_to_grade(69.5)).to eql("C")
expect(standard.score_to_grade(69.499)).to eql("D")
expect(standard.score_to_grade(60)).to eql("D")
expect(standard.score_to_grade(55.5)).to eql("D")
expect(standard.score_to_grade(54.5)).to eql("E")
expect(standard.score_to_grade(50)).to eql("M")
expect(standard.score_to_grade(0)).to eql("M")
expect(standard.score_to_grade(-100)).to eql("M")