14
\$\begingroup\$

Given a set of letter grades, output the GPA (grade point average) of those grades, rounded to one decimal place.

Valid grades and their corresponding value are the following:

A = 4 grade points
A- = 3.7 grade points
B+ = 3.3 grade points
B = 3 grade points
B- = 2.7 grade points
C+ = 2.3 grade points
C = 2 grade points
C- = 1.7 grade points
D+ = 1.3 grade points
D = 1 grade point
D- = 0.7 grade points
F = 0 grade points

The formula for GPA is simply the average of the points values of the grades. The rounding scheme used should be round half up (i.e. less than 0.05 gets rounded down to 0 and greater than or equal to 0.05 gets rounded up to 0.1). The average should be unweighted (all inputs are treated equally). Note that the same grade can show up multiple times in the input, in which case each instance of that grade should be a separate element in the average.

So for example, if the input was [A, B+, A, C-], the GPA is (4 + 3.3 + 4 + 1.7)/4 = 3.25, which rounds to 3.3.

The program should take as input the grades to be averaged using whatever format is convenient, such as a list, or a string with a delimiter of your choice. Each input grade should be a single string (i.e. the letter components and +/- shouldn't be separate inputs). You can assume the input will always contain at least one grade. Note that grades may be repeated in the input.

Output can optionally include the ".0" suffix in the event of an integer result but this is not required.

This is code golf, so shortest solution in bytes wins.

Test cases:

[A] => 4
[F, F, F] => 0
[D+, D-] => 1
[A, B, C, D] => 2.5
[A-, B-, C-, D-] => 2.2
[A, A-, B+, B, B-] => 3.3
[A, B+, A, C-] => 3.3
[C+, C+, A, C+] => 2.7
[A, B, F, F] => 1.8
\$\endgroup\$
9
  • 1
    \$\begingroup\$ So this is just replacing the grades with their corresponding values, and averaging? \$\endgroup\$
    – Adám
    Sep 14 at 21:51
  • 1
    \$\begingroup\$ "Input can be of whatever format is convenient" — Can we take the letters as one argument and the signs as another? \$\endgroup\$
    – Adám
    Sep 14 at 22:11
  • \$\begingroup\$ can we take the letters in lowercase \$\endgroup\$ Sep 14 at 22:34
  • \$\begingroup\$ Are we allowed to output 4.0 instead of 4? \$\endgroup\$ Sep 14 at 23:22
  • 2
    \$\begingroup\$ @MatthewJensen having the trailing ".0" is fine \$\endgroup\$
    – Zags
    Sep 15 at 15:11

17 Answers 17

22
\$\begingroup\$

JavaScript (ES6), 67 bytes

Returns a number.

a=>(a.map(([x,y])=>t+=940.5%(n++,'0x5'+x)%85+~~(y+3),n=t=0)|t/n)/10

Try it online!

How?

The purpose of the black-magic formula 940.5 % ('0x5' + x) % 85 is to turn a grade into its base value pre-multiplied by \$10\$ and with an offset of \$1/2\$ for the final rounding.

x N = '0x5' + x As decimal 940.5 mod N mod 85
A 0x5A 90 40.5 40.5
B 0x5B 91 30.5 30.5
C 0x5C 92 20.5 20.5
D 0x5D 93 10.5 10.5
F 0x5F 95 85.5 0.5

We use the expression ~~(y + 3) to add an additional offset of \$\pm 3\$ if the grade is followed by + or -. This evaluates to ~~NaN (which is \$0\$) if there's no modifier.

Given the sum \$t\$ of all these values, the GPA is given by \$\lfloor t/n\rfloor/10\$, where \$n\$ is the number of grades.


JavaScript (ES6), 76 bytes

Original answer, returning a string.

a=>(eval(a.map(s=>s<'F'&&'14-0x'+s+[s[1]&&.3]).join`+`)/a.length).toFixed(1)

Try it online!

How?

If the grade is "F", we turn it into "false".

Otherwise:

  • we add the prefix "14-0x"
  • if this is a two-character grade, we add the suffix "0.3"

Examples:

  • "A" is turned into "14-0xA" (\$14-10=4\$)
  • "B+" is turned into "14-0xB+0.3" (\$14-11+0.3=3.3\$)
  • "C-" is turned into "14-0xC-0.3" (\$14-12-0.3=1.7\$)
\$\endgroup\$
6
  • \$\begingroup\$ Given the strict output format, it might be worth checking that 4.0 output is allowed \$\endgroup\$ Sep 14 at 23:22
  • \$\begingroup\$ @MatthewJensen output formats are generally lax on this site \$\endgroup\$
    – Seggan
    Sep 15 at 0:43
  • 6
    \$\begingroup\$ Really unexpected and creative. \$\endgroup\$
    – Jonah
    Sep 15 at 3:50
  • \$\begingroup\$ 76 for the second one: a=>(eval(a.map(s=>s<'F'&&'140-10*0x'+s+[s[1]&&3]).join+)/a.length+.5|0)/10 \$\endgroup\$
    – tsh
    Sep 15 at 3:53
  • \$\begingroup\$ @tsh Nice. Saved 3 bytes from there. \$\endgroup\$
    – Arnauld
    Sep 15 at 8:38
8
\$\begingroup\$

Excel, 89 84 bytes

Saved 5 bytes thanks to JvdV

=ROUND(AVERAGE(IF(A:A=0,"",FIND(LEFT(A:A),"FDCBA")-1+IFERROR(--RIGHT(A:A)&.3,0))),1)

Input is in the first column. One grade per row. When you paste the formula, Excel will automatically add a leading zero so .3 becomes 0.3.

  • ROUND(~,1) will round the final result to one decimal place.
  • AVERAGE(~) will average the array we're about to calculate.
  • IF(A:A=0,"",~) will ignore all the blank cells below the input. Without this, the above solution produces around 1 million zeroes so that throws off the average a bit.
  • BASE VALUE: FIND(LEFT(A:A),"FDCBA")-1 get the base GPA value of each letter from 0 - 4.
  • ADJUSTMENT VALUE: IFERROR(--RIGHT(A:A)&.3,0)
    • RIGHT(A:A)&.3 combines the right-most character (which may be a letter) with .3.
    • --~ tries to coerce that value into a number which throws an error if the right-most character was a letter since something like --A.3 is not a number.
    • IFERROR(~,0) corrects all those errors to 0 instead.
  • When you add the base value and adjustment value (which may be negative), you get the GPA value for that letter grade.
  • All those grades are calculated individually and then averaged and rounded to one decimal place.

Screenshot

\$\endgroup\$
1
  • 1
    \$\begingroup\$ Note that you can change VALUE() to a double unary too saving a few bytes. Nice sollution once again. \$\endgroup\$
    – JvdV
    Sep 20 at 12:25
5
\$\begingroup\$

APL(Dyalog Unicode), 38 bytes SBCS

Anonymous prefix lambda taking a string argument of whatever format is convenient.

{1⍕(+/l,.3×-⌿'+-'∘.=⍵)÷≢l←0⌈5-⎕A⍳⍵∩⎕A}

Try it on APLgolf!

{} "dfn"; argument is :

⍵∩⎕A intersection of argument and uppercase Alphabet

⎕A⍳ indices of those letters in the uppercase Alphabet (A=1)

5- subtract those indices from 5

0⌈ maximum of 0 and those numbers

l← assign to l

 count those

( divide the following by that:

  '+-'∘.=⍵ comparison table of signs vs the argument characters

  -⌿ subtract the bottom row from the top row

  .3× multiply those differences by 0.3

  l, prepend l

+/ sum

1⍕ format as string with 1 decimal

\$\endgroup\$
5
\$\begingroup\$

Python, 98 bytes

lambda i:round(sum(x*i.count(y)for y,x in zip("FDCBA-+",[*range(5),-.3,.3]))/len(i.split())+.01,1)

Attempt This Online!

\$\endgroup\$
2
  • \$\begingroup\$ Welcome to Code Golf, and nice answer! \$\endgroup\$ Sep 15 at 21:50
  • \$\begingroup\$ The +.01 fudge to get rounding up is creative, but it won't work on sufficiently large inputs (ones with 20 grades for example). Using Python2 (which had different rounding semantics) could do this correctly and save a few bytes. Alternatively, in Python3: int(10*...+0.5)/10 \$\endgroup\$
    – Zags
    Sep 19 at 17:23
5
\$\begingroup\$

Python 3.8, 95 93 92 bytes

s=.01;i=0
for g,a,*_ in open(0):i+=1;s+='FDCBA'.find(g);exec(f's=s{a}.3')
exit(f'{s/i:.1f}')

Input (stdin)

A
B+
C
D

Very straightforward. Open stdin with open(0), then read each line with for g. Python has an annoying property where it leaves a trailing \r after each line, which we exploit in an f-string passed to exec. In case there's no appendix after grade, f-string becomes s=s\r.3, which is essentially a no-op. Very unsafe ;)

\$\endgroup\$
2
  • 1
    \$\begingroup\$ Welcome to Code Golf, and nice answer! \$\endgroup\$ Sep 17 at 22:33
  • \$\begingroup\$ -5 bytes \$\endgroup\$
    – xnor
    Sep 20 at 7:10
4
\$\begingroup\$

sed -E, 302 299 bytes

:a
s/ //
s/[A-D]/&aaaaaaaaaa/g
y/ABCD/BCDF/
s/\+/aaa/
/-/{s/-//;s/a//;s/a//;s/a//}
s/Fa/aF/
ta
s/F/!F/
:b
s/F/a/
s/(a+)(a*!)\1$/X\2\1/
tb
s/!.*/\U&/
s/AA?/a/g
/(a+)!\1$/s/a/X/
s/[a!]//g
s/X{10}/Y/g
s/$/I.0/
:c
/X/y/012345678/123456789/
/Y/y/IJKLMNOPQ/JKLMNOPQ9/
s/X//
s/Y//
tc
y/IJKLMNOPQ/012345678/

Attempt This Online!

Explanation:

                  # store dividend as unary "a" and divisor as unary "F"
:a
s/ //                 # removes all spaces
s/[A-D]/&aaaaaaaaaa/g # for each of letters ABCD ten "a" are added
y/ABCD/BCDF/          # each of letters ABCD are substracted to BCDE
s/\+/aaa/             # for every + three "a" are added
/-/{s/-//;s/a//;s/a//;s/a//} # for every - three "a" are removed
s/Fa/aF/              # sort the string into format "aaaaaFFFFF"
ta
                  # now perform the division
s/F/!F/               # transforms string to format "aaaaa!FFFFF"
:b
s/F/a/                # transforms string to format "aaaaa!aaaaa"
s/(a+)(a*!)\1$/X\2\1/ # if there are more or equal "a" on left side than on the right append "X" to beggining
tb
                  # now do the rounding
s/!.*/\U&/            # transorm "a" after "!" to "A"
s/AA?/a/g             # now the amount of "A" is divided by 2
/(a+)!\1$/s/a/X/      # check if the reminder is more or equal to half of divisor and if so, then add one more "X"
s/[a!]//g             # remove leftover characters
                  # now the result is stored as unary "X"
                  # conversion from unary to decimal
s/X{10}/Y/g           # convert every ten "X" to "Y"
s/$/I.0/              # adds string for output "I.0"
:c
/X/y/012345678/123456789/ # for every "X" the "0" is increased
/Y/y/IJKLMNOPQ/JKLMNOPQ9/ # for every "Y" the "I" is increased
s/X//
s/Y//
tc
y/IJKLMNOPQ/012345678/ # now the "I" is transformed to digit
\$\endgroup\$
4
  • \$\begingroup\$ Nice. Mind adding a commented version? Or high-level explanation of approach... \$\endgroup\$
    – Jonah
    Sep 15 at 19:48
  • 1
    \$\begingroup\$ @Jonah Added the commented version. \$\endgroup\$
    – Jiří
    Sep 15 at 20:58
  • 1
    \$\begingroup\$ despite it's practical cmdline uses, sed truly is an esolang, and a fascinating one. there is no question that it is harder to program in than, say, brainfuck. \$\endgroup\$
    – Jonah
    Sep 15 at 21:57
  • \$\begingroup\$ @Jonah Hehe, I actually started programming in sed, because I consider it as easier version of brainfuck. \$\endgroup\$
    – Jiří
    Sep 15 at 22:24
4
\$\begingroup\$

Python 3, 93 bytes

lambda l:int(0.5+sum(3*(c=='+')-3*(c=='-')or'FDCBA'.index(c)*10for c in''.join(l))/len(l))/10

Takes input as a list of strings (['A', 'B+']).

\$\endgroup\$
4
\$\begingroup\$

Pyth, 24 29 bytes

.OmeS,0v++"14-0x"d?tld".3";.z

Try it online!

\$\endgroup\$
2
  • 1
    \$\begingroup\$ Running with A\nB\nC\nD as the input gives me output 4\n\n0\n\n0\n\n0; should output 2.5 \$\endgroup\$
    – Zags
    Sep 15 at 16:53
  • \$\begingroup\$ @Zags Well that's embarrassing, I somehow lost track of what the question was... fixed \$\endgroup\$
    – hakr14
    Sep 17 at 3:06
3
\$\begingroup\$

Jelly, 24 bytes

ON%7’»0_2¦€4ḋ10,3Æm+.Ḟ÷⁵

A monadic Link that accepts a list of lists of characters and yields a number.

Try it online!

How?

ON%7’»0_2¦€4ḋ10,3Æm+.Ḟ÷⁵ - Link: list of grades  e.g. ['A-', 'B+', 'F', 'F']
O                        - ordinals (vectorises)      [[65, 45], [66, 43], [70], [70]]
 N                       - negate                     [[-65, -45], [-66, -43], [-70], [-70]]
  %7                     - mod seven                  [[5, 4], [4, 6], [0], [0]]
    ’                    - decrement                  [[4, 3], [3, 5], [-1], [-1]]
     »0                  - max with zero              [[4, 3], [3, 5], [0], [0]]
        2¦€              - from 2nd of each:
       _   4             -   subtract four            [[4, -1], [3, 1], [0], [0]]
            ḋ10,3        - dot product with [10,3]    [37, 33, 0, 0]
                 Æm      - mean                       17.5
                   +.    - add a half                 18.0
                     Ḟ   - floor                      18
                      ÷⁵ - divide by ten              1.8

If banker's rounding was used \$21\$ bytes is possible with ON%7’»0_2¦€4Uḅ.3Æmær1 (U reverses each, ḅ.3 converts from base \$0.3\$, and ær1 uses Python's round to round to 1 decimal place).

\$\endgroup\$
3
\$\begingroup\$

05AB1E, 30 26 bytes

ε14sáC-.3y¦'\ìθ.VDd*}ÅA1.ò

-4 bytes by semi-porting @Arnauld's JavaScript answer

Try it online or verify all test cases.

Original 30 bytes answer:

•H₁yΔи•.¥RA4£…+ -â¦ðмuIkèÅAòT/

Try it online or verify all test cases.

Explanation:

ε            # Map over the (implicit) input-list:
 14          #  Push 14
   s         #  Swap so the current grade-string is at the top
    á        #  Only keep its letters (removing a potential "+"/"-")
     C       #  Convert it to 'binary': "A"=10, "B"=11, "C"=12, "D"=13, "F"=15
      -      #  Subtract it from the 14
 .3          #  Push 0.3
   y         #  Push the current grade-string again
    ¦        #  Remove its first character (the letter)
     '\ì    '#  Prepend a leading "\"
        θ    #  Pop and only keep its last character
         .V  #  Execute it as 05AB1E code
             #  ("+"/"-" does what you'd expect; "\" discards the 0.3 from the stack)
 D           #  Duplicate the decimal
  d          #  Check if it's non-negative (1 if >=0; 0 if <0)
   *         #  Multiply that to the decimal to change the "F"=1 to 0
}            # Close the map
 ÅA          # Get the average of this list
   1.ò       # Round it to 1 decimal point
             # (after which the result is output implicitly)
•H₁yΔи•      # Push compressed integer 73343343343
       .¥    # Undelta its digits with leading 0: [0,7,10,13,17,20,23,27,30,33,37,40]
         R   # Reverse it: [40,37,33,30,27,23,20,17,13,10,7,0]
A            # Push the lowercase alphabet
 4£          # Only keep its first four characters: "abcd"
   …+ -      # Push string "+ -"
       â     # Get the cartesian product of the two: ["a+","a ","a-",...,"d+","d ","d-"]
        ¦    # Remove the leading "a+"
         ðм  # Remove all spaces from each string
Il           # Push the input-list, and convert each grade-string to lowercase
  k          # Get the indices of each lowercase grade-string in the string-list we've created)
             # (-1 for "f", since it's not in the list)
  è          # Use that to index into the earlier list of integers
             # (-1 wraps around to the last item, which is the 0)
   ÅA        # Get the average of this list
     ò       # Round it to an integer
      T/     # Divide it by 10
             # (after which the result is output implicitly)

See this 05AB1E tip of mine (section How to compress large integers?) to understand why •H₁yΔи• is 73343343343.

\$\endgroup\$
3
\$\begingroup\$

C (clang), 113 96 bytes

-17 bytes thanks to celingcat and jdt

k;i;main(j){for(;gets(&j);k++)i+=j%8<5?50-j%8*10:0,i+=(j>>=8)?132-j*3:0;printf("%.1f",i/10./k);}

Try it online!

Attempt This Online!

Takes input as one grade per line.

Explanation

gets(&j)

Read the string into a integer field j. This is perfectly valid in C, and is a compact way to read strings of upto 3 bytes long.

j%8<5

Check for F

50-j%8*10

Extract the last byte from the string, this will be the letter (since little endian). 178-10*char will be the letter grade, 40, 30, 20, 10 etc.

0,

I think this just changed the operator precidence? Not sure.

i+=(j>>=8)?132-j*3:0

Add or subtract 3 depending on the sign, since + is 43 and - is 45

printf("%.1f",i/10./k)

Round and print the final number as a float.

\$\endgroup\$
0
2
\$\begingroup\$

Red, 124 bytes

func[a][forall a[parse a/1[change p: skip(rejoin[max 69 - p/1 0" "])opt[skip insert" .3"]]a/1: do a/1]round/to average a .1]

Try it online!

More readable:

f: func [a][
    forall a [
        parse a/1 [
            change set g skip (rejoin [max 69 - g 0 space])
            opt [skip insert " 0.3"]
        ]
        a/1: do a/1
    ]
    round/to average a 0.1
]

The input is a block (list) of strings. I modify each string in place by changing the letter with the corresponding grade value and optionally appending "0.3" after + or -. Then the string is replaced by its evaluated value. Finally I find and round the average of the block.

\$\endgroup\$
2
\$\begingroup\$

Retina 0.8.2, 90 bytes

T`L`4-0
\d
0$&$*
1
20$*
\+
6$*
1{6}-

O`.
0+
$.&$*1$.&$*1,$.&$*
\D*(1+),(\1)*1*
$#2
.$
.$&

Try it online! Link includes test cases. Output always includes the decimal but omits the leading 0 on scores of less than 1. Explanation:

T`L`4-0

Translate uppercase letters to digits A=4, B=4, C=2, D=1, otherwise 0.

\d
0$&$*

Convert to unary and prefix a marker 0 to keep count of the number of grades.

1
20$*

Multiply by 20.

\+
6$*

Add 6 for a +.

1{6}-

Subtract 6 for a -.

O`.

Collect the markers and scores together.

0+
$.&$*1$.&$*1,$.&$*

Add the count to the sum (so that the division will round) and double the count.

\D*(1+),(\1)*1*
$#2

Divide the sum by the doubled count.

.$
.$&

Divide by 10.

\$\endgroup\$
2
\$\begingroup\$

PHP, 165 bytes

function($a){$i='array_search';$g=str_split("FDCBA");$r=0;foreach($a AS$v){@$r+=eval("return {$i($v[0], $g)}".($v[1]?$v[1].".3;":";"));}die(round($r/count($a),1));};

Try it online!

This is my first time golfing in PHP, so I probably missed a few places where I could have been more efficient.

Explanation

PHP is completely and utterly broken, which is why golfing in it is so fun. We approach this problem by using a string and splitting it, using the index as GPA value. The array_search function grabs the proper GPA value for us. We then use the + or - in the input to add or subtract 0.3 to the GPA value and use a standard algorithm to find the average value.

\$\endgroup\$
5
  • \$\begingroup\$ 110 bytes \$\endgroup\$
    – Steffan
    Sep 17 at 18:19
  • \$\begingroup\$ (You can always remove the @s, as outputting junk to STDERR is perfectly fine.) \$\endgroup\$
    – Steffan
    Sep 17 at 18:20
  • \$\begingroup\$ One tip I used here was removing $r=0; - PHP is really cursed (of course) and lets you add to unassigned variables. Another was using strpos instead of array_search and str_split. (Sorry, but not using eval was shorter.) \$\endgroup\$
    – Steffan
    Sep 17 at 18:23
  • \$\begingroup\$ I did also change the die to echo because it would be invalid by our rules, as functions need to be reusable, and not quit the script. (It didnt' cost bytes though.) \$\endgroup\$
    – Steffan
    Sep 17 at 18:27
  • \$\begingroup\$ 100 bytes \$\endgroup\$
    – Steffan
    Sep 17 at 18:33
2
\$\begingroup\$

Knight (v2.0-alpha), 81 bytes

O+++=s=n"";W=gP;=n+1n=s++*100&!?70=cA[g-69c*30&]g-44A]g s/=q/+/s n 5=t 10t'.'%q t

Try it online!

The basic algorithm is as follows, with a few golfing optimizations added:

# Set `s` and `n` to the empty string. This could be any zero value,3
# but an empty string helps with `OUTPUT`
; + = s = n ""

# Each grade is on a separate line
; WHILE = grade PROMPT
    ; = n + 1 n # increment the amount of grades

    # The grade part is `100 * (c == 'F' ? 0 : 69 - c)`, 
    # ie the grade offset for normal grades, or `0` for `F`.
    ; = grade_part * 100 &(!? 70 = c ASCII [grade) (- 69 c) #nice

    # The modifier is `grade.len == 1 ? 0 : 30 * (44 - grade[1].ascii)`
    ; = modifier_part * 30 (& ]grade (- 44 ASCII ]grade))

    # `s` will be coerced to an int the first tiem around.
    : = sum ++ grade_part modifier_part s

# Calculate the average * 10. To accommodate the rounding rule,
# we add five, then divide (`sum`, which is 100x a GPA) by ten.
; = avg / (+ /sum n 5) 10

# Now, simply output the number in the float format
: OUTPUT ++(+ "" /avg 10) '.' (% avg 10)

```
\$\endgroup\$
1
\$\begingroup\$

Charcoal, 34 bytes

WS⊞υ⁺⌕FDCBA§ι⁰×·³⁻№ι+№ι-﹪%.1f∕ΣυLυ

Try it online! Link is to verbose version of code. Takes input as a list of newline-terminated grade strings. Explanation:

WS

Repeat for each input grade...

⊞υ⁺⌕FDCBA§ι⁰×·³⁻№ι+№ι-

... calculate its value by looking up the letter in the string FDCBA and adjusting by 0.3 depending on the number of +s or -s.

﹪%.1f∕ΣυLυ

Take the average and format it to one decimal place.

\$\endgroup\$
1
\$\begingroup\$

Python 3, 168 150 bytes

m={'A':4,'A-':3.7,'B+':3.3,'B':3,'B-':2.7,'C+':2.3,'C':2,'C-':1.7,'D+':1.3,'D':1,'D-':0.7,'F':0}
g=lambda n:int(sum([m[i]for i in n])/len(n)*10+.5)/10

Could save some bytes on the dictionary creation.

Returns a float.

\$\endgroup\$
2
  • \$\begingroup\$ You should use int(x*10+.5)/10 instead of math.ceil. It's shorter and fixes a bug (your current solution turn 0.01 into 0.1, which is incorrect). \$\endgroup\$
    – Zags
    Sep 19 at 17:12
  • \$\begingroup\$ you could also save some bytes by packing into a one-line lambda that takes a list as an argument. you then don't need to print the output or call split on the input, and can skip some variable assignments. Note also that you can reference a key into a dictionary without it needing a variable (for example: {"A": 4, "B": 3}["A"] is valid Python) \$\endgroup\$
    – Zags
    Sep 20 at 13:46

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.