Fun with rating stars
Published: 2023-01-25 12:00AM
Star Ratings
A recent tweet showed some C# code for producing a String of stars that might be used when displaying ratings on a website:
Let’s have a look at several ways to do the same thing in Groovy.
def rating0(percentage) {
int stars = Math.ceil(percentage * 10)
("π΅" * stars).padRight(10, "βͺ")
}
def rating1(percentage) {
int stars = Math.ceil(percentage * 10)
"π΅" * stars + "βͺ" * (10-stars)
}
def rating2(percentage) {
int skip = 10 - Math.ceil(percentage * 10)
"π΅π΅π΅π΅π΅π΅π΅π΅π΅π΅βͺβͺβͺβͺβͺβͺβͺβͺβͺβͺ"[skip..<10+skip]
}
def rating3(percentage) {
switch(percentage) {
case 0 -> "βͺβͺβͺβͺβͺβͺβͺβͺβͺβͺ"
case { it <= 0.1 } -> "π΅βͺβͺβͺβͺβͺβͺβͺβͺβͺ"
case { it <= 0.2 } -> "π΅π΅βͺβͺβͺβͺβͺβͺβͺβͺ"
case { it <= 0.3 } -> "π΅π΅π΅βͺβͺβͺβͺβͺβͺβͺ"
case { it <= 0.4 } -> "π΅π΅π΅π΅βͺβͺβͺβͺβͺβͺ"
case { it <= 0.5 } -> "π΅π΅π΅π΅π΅βͺβͺβͺβͺβͺ"
case { it <= 0.6 } -> "π΅π΅π΅π΅π΅π΅βͺβͺβͺβͺ"
case { it <= 0.7 } -> "π΅π΅π΅π΅π΅π΅π΅βͺβͺβͺ"
case { it <= 0.8 } -> "π΅π΅π΅π΅π΅π΅π΅π΅βͺβͺ"
case { it <= 0.9 } -> "π΅π΅π΅π΅π΅π΅π΅π΅π΅βͺ"
default -> "π΅π΅π΅π΅π΅π΅π΅π΅π΅π΅"
}
}
If you want you can test the various edge cases:
for (i in 0..3)
for (j in [0, 0.09, 0.1, 0.11, 0.9, 1])
println "rating$i"(j)
Increasing Robustness
The code examples here assume that the input is in the range 0 <= percentage <= 1
. There are several tweaks we could do to guard against inputs outside those ranges.
We could simply add an assert, e.g.:
def rating0b(percentage) {
assert percentage >= 0 && percentage <= 1
int stars = Math.ceil(percentage * 10)
("π΅" * stars).padRight(10, "βͺ")
}
Or, if we wanted to not fail, tweak some of our conditions, e.g. for
rating3
, instead of case 0
, we could use case { it ⇐ 0 }
.
We could push the checks into our types by making a special class, Percent
say, which only permitted the allowed values:
final class Percent {
final Double value
Percent(Double value) {
assert value >= 0 && value <= 1
this.value = value
}
}
And we could optionally also use some metaprogramming to provide a custom isCase
method:
Double.metaClass.isCase = { Percent p -> delegate >= p.value }
Which means we could tweak the rating method to be:
def rating3b(Percent p) {
switch(p) {
case 0.0d -> "βͺβͺβͺβͺβͺβͺβͺβͺβͺβͺ"
case 0.1d -> "π΅βͺβͺβͺβͺβͺβͺβͺβͺβͺ"
case 0.2d -> "π΅π΅βͺβͺβͺβͺβͺβͺβͺβͺ"
case 0.3d -> "π΅π΅π΅βͺβͺβͺβͺβͺβͺβͺ"
case 0.4d -> "π΅π΅π΅π΅βͺβͺβͺβͺβͺβͺ"
case 0.5d -> "π΅π΅π΅π΅π΅βͺβͺβͺβͺβͺ"
case 0.6d -> "π΅π΅π΅π΅π΅π΅βͺβͺβͺβͺ"
case 0.7d -> "π΅π΅π΅π΅π΅π΅π΅βͺβͺβͺ"
case 0.8d -> "π΅π΅π΅π΅π΅π΅π΅π΅βͺβͺ"
case 0.9d -> "π΅π΅π΅π΅π΅π΅π΅π΅π΅βͺ"
default -> "π΅π΅π΅π΅π΅π΅π΅π΅π΅π΅"
}
}
We can be fancier here using @EqualsAndHashCode
on the Percent
class and/or using @Delegate
on the value
property, depending on how rich in
functionality we wanted the Percent
instances to be.
Alternatively, we could use a design-by-contract approach:
@Requires({ percentage >= 0 && percentage <= 1 })
def rating1b(percentage) {
int stars = Math.ceil(percentage * 10)
"π΅" * stars + "βͺ" * (10-stars)
}