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:
parent
8fabb21626
commit
2876c583e3
|
@ -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)}%"
|
||||
|
|
|
@ -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
|
|
@ -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]
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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")
|
||||
|
|
Loading…
Reference in New Issue