bookstore : add to track #310
Conversation
|
Renamed to make clear the status of this PR. @Twisti, when you're ready for a review, please rename it back to |
|
Ready for a review :) |
| break; | ||
| case 5: | ||
| discountPercentage = 25; | ||
| break; |
There was a problem hiding this comment.
I see a pretty clear relationship between groupSize and discountPercentage. Might there be a more concise, but equally readable, way to capture this?
Also wondering, is this really configuration data rather than business logic?
There was a problem hiding this comment.
I don't the the relationship between groupSize and discountPercentage, can you tell me more about this?
Also I didn't understand your second question, can you develop and explain it ?
There was a problem hiding this comment.
I just checked the php exemple of the exercise and saw a different version about the discount percentage :
$discount = [0, 0, 0.05, 0.1, 0.2, 0.25];
$price = 8;
return $price * $count * (1 - $discount[$count]);
So in Java, it will be :
double[] discountPercentage = {0, 0, 0.05, 0.1, 0.2, 0.25};
return 8 * groupSize * (1 - discountPercentage[groupSize]);
It's this kind of relationship you're talking about ?
There was a problem hiding this comment.
Or even simpler. Perhaps take it in two steps:
- Check for invalid group size and throw and exception if invalid
- return for the discounted group price:
8 * (groupSize - 1) * 0.05d;Even better, avoid the use of magic numbers. Name the book price and discount increment, EG DISCOUNT_INCREMENT, BOOK_PRICE, define them in the class, and use:
BOOK_PRICE * (groupSize - 1) * DISCOUNT_INCREMENT;There was a problem hiding this comment.
As far as my "configuration vs logic" comment, it's not hard to imagine that the price of a book or the discount increment would be a business rule of the bookstore. In a production application, you would want to separate setting those values from the logic. You might even imagine a case in which you would have a class that is responsible for calculating the discount that is separate from the one that applies it to the user's purchase.
In this example, I feel like calling these values out by defining them at the class level with clear names is probably enough.
There was a problem hiding this comment.
8 * (groupSize - 1) * 0.05d;Are you sure that this work ? Or maybe I'm missing something ?
My method looks like this :
private double CostPerGroup(int groupSize) throws Exception{
if(groupSize > maxGroupSize || groupSize <= 0 )
throw new Exception("Invalide group size : " + groupSize );
double discountForOneBook = bookPrice * (groupSize - 1) * discountIncrement;
return (bookPrice * groupSize ) - (groupSize * discountForOneBook);
}But by running gradle for the test, I have 13 tests completed, 5 failed :
BookStoreTest > TwoCopiesOfEachBook FAILED
java.lang.AssertionError: expected:<60.0> but was:<64.0>
at org.junit.Assert.fail(Assert.java:88)
at org.junit.Assert.failNotEquals(Assert.java:834)
at org.junit.Assert.assertEquals(Assert.java:553)
at org.junit.Assert.assertEquals(Assert.java:683)
at BookStoreTest.TwoCopiesOfEachBook(BookStoreTest.java:106)
BookStoreTest > TwoEachOfFirst4BooksAnd1CopyEachOfRest FAILED
java.lang.AssertionError: expected:<55.6> but was:<59.2>
at org.junit.Assert.fail(Assert.java:88)
at org.junit.Assert.failNotEquals(Assert.java:834)
at org.junit.Assert.assertEquals(Assert.java:553)
at org.junit.Assert.assertEquals(Assert.java:683)
at BookStoreTest.TwoEachOfFirst4BooksAnd1CopyEachOfRest(BookStoreTest.java:97)
BookStoreTest > ThreeEachOFirst2BooksAnd2EachOfRemainingBooks FAILED
java.lang.AssertionError: expected:<75.2> but was:<79.2>
at org.junit.Assert.fail(Assert.java:88)
at org.junit.Assert.failNotEquals(Assert.java:834)
at org.junit.Assert.assertEquals(Assert.java:553)
at org.junit.Assert.assertEquals(Assert.java:683)
at BookStoreTest.ThreeEachOFirst2BooksAnd2EachOfRemainingBooks(BookStoreTest.java:124)
BookStoreTest > ThreeCopiesOfFirstBookAnd2EachOfRemaining FAILED
java.lang.AssertionError: expected:<68.0> but was:<72.0>
at org.junit.Assert.fail(Assert.java:88)
at org.junit.Assert.failNotEquals(Assert.java:834)
at org.junit.Assert.assertEquals(Assert.java:553)
at org.junit.Assert.assertEquals(Assert.java:683)
at BookStoreTest.ThreeCopiesOfFirstBookAnd2EachOfRemaining(BookStoreTest.java:115)
BookStoreTest > TwoGroupsOfFourIsCheaperThanGroupOfFivePlusGroupOfThree FAILED
java.lang.AssertionError: expected:<51.2> but was:<53.6>
at org.junit.Assert.fail(Assert.java:88)
at org.junit.Assert.failNotEquals(Assert.java:834)
at org.junit.Assert.assertEquals(Assert.java:553)
at org.junit.Assert.assertEquals(Assert.java:683)
at BookStoreTest.TwoGroupsOfFourIsCheaperThanGroupOfFivePlusGroupOfThree(BookStoreTest.java:79)
So, I prefer to upload this modifications and don't push now.
BookStore_Java_Twisti.tar.gz
There was a problem hiding this comment.
Sorry, I made an error! I thought the discount tiers were linear by groupSize, and they are not. My proposal and the file you uploaded both have a problem because of that. However, a solution like the PHP one you mentioned would work nicely:
private static double[] DISCOUNT_TIERS = {0,5,10,20,25}; //declare at class level
private double CostPerGroup(int groupSize) throws Exception{
if (groupSize < 1 || groupSize > 5){
throw new Exception("Invalid group size : " + groupSize );
}
return bookPrice * groupSize * (100 - DISCOUNT_TIERS[groupSize-1])/100;
}| assertEquals(30,BookStore.CalculateTotalCost(books),2); | ||
| } | ||
|
|
||
|
|
There was a problem hiding this comment.
Looks like from this point on, the test suite diverges from the canonical test suite for this exercise. Is that intentional?
All the remaining tests are extra, and these tests from the canonical spec are missing:
{
"description": "Two groups of four is cheaper than group of five plus group of three",
"basket": [1,1,2,2,3,3,4,5],
"targetgrouping": [[1,2,3,4],[1,2,3,5]],
"expected": 51.20
},
{
"description": "Group of four plus group of two is cheaper than two groups of three",
"basket": [1,1,2,2,3,4],
"targetgrouping": [[1,2,3,4],[1,2]],
"expected": 40.8
},
{
"description": "Two each of first 4 books and 1 copy each of rest",
"basket": [1,1,2,2,3,3,4,4,5],
"targetgrouping": [[1,2,3,4,5],[1,2,3,4]],
"expected": 55.60
},
{
"description": "Two copies of each book",
"basket": [1,1,2,2,3,3,4,4,5,5],
"targetgrouping": [[1,2,3,4,5],[1,2,3,4,5]],
"expected": 60.00
},
{
"description": "Three copies of first book and 2 each of remaining",
"basket": [1,1,2,2,3,3,4,4,5,5,1],
"targetgrouping": [[1,2,3,4,5],[1,2,3,4,5],[1]],
"expected": 68.00
},
{
"description": "Three each of first 2 books and 2 each of remaining books",
"basket": [1,1,2,2,3,3,4,4,5,5,1,2],
"targetgrouping": [[1,2,3,4,5],[1,2,3,4,5],[1,2]],
"expected": 75.20
}There was a problem hiding this comment.
I followed the test from the C# version, so this test is just a translation from C# to Java.
I will update my tests to add tests you have linked.
There was a problem hiding this comment.
Yes, I see that!
Thanks for adding the additional tests. Along the way, before we merge your changes, we'll need you to add the annotation @Ignore before all but the first test in the suite. That way users can focus on getting one test at a time to pass.
|
@Twisti thanks for doing this! In the spirit of challenging each other to produce the best possible product, I have two questions for you. Please let me know your thoughts. Also, we merged some older, pending PRs this morning and that created a merge conflict with your |
| public class BookStoreTest{ | ||
|
|
||
| @Test | ||
| public void Only_a_single_book(){ |
There was a problem hiding this comment.
Standard convention in Java is to use camelCase for method and variable names. Save the snake_case for Python!
| @Test | ||
| public void Only_a_single_book(){ | ||
| Integer[] p = {1}; | ||
| ArrayList<Integer> books = new ArrayList<>(Arrays.asList(p)); |
|
@Twisti thanks for your additional efforts on this one. I have made some specific comments in response to your questions, and called out a few more things that should be updated to conform to Java 'best practices.' Finally, I must draw your attention to a conversation that some of the maintainers had around another exercise. Specifically, it was about preferring the use of instance methods rather than class methods. If you are comfortable making this change in your PR, I think it would be good. Please let me know if you would like help with any of these things, I'd be pleased to assist. |
|
As you advised for me to do, I updated my code :
|
There was a problem hiding this comment.
@Twisti this is really coming along nicely!
Please consider the feedback I have given you for the discount method, but know that it is optional.
The Java style changes would be appreciated, as they are the standard here on exercism/xjava.
I've made a few comments about capitalization that apply in several places, but I didn't think it necessary to mention them all. They will need to be addressed in both the example code and the test code please.
Please let me know if I can assist you in any way.
| double minPrice = Double.MAX_VALUE; | ||
|
|
||
|
|
||
| if(books.size() == 0) |
There was a problem hiding this comment.
Stylistically we would prefer if you use braces, even for simple cases like this. EG:
if (books.size() ==0){
return priceSoFar;
}| this.bookPrice = 8; | ||
| } | ||
|
|
||
| public double CalculateTotalCost(){ |
There was a problem hiding this comment.
Method names should be camelCase, but not CapitalWords in Java. Save the CapitalWords for class names.
| import java.util.List; | ||
| import java.util.stream.Collectors; | ||
|
|
||
| public class BookStore{ |
There was a problem hiding this comment.
In English we would say Bookstore rather than BookStore. Also, the folder name should likewise be one word bookstore without the hypen.
| if(books.size() == 0) | ||
| return priceSoFar; | ||
|
|
||
| List<Integer> groups = (ArrayList<Integer>) books.stream().distinct().collect(Collectors.toList()); |
| discountPercentage = 25; | ||
| break; | ||
| default: | ||
| throw new Exception("Invalide group size : " + groupSize ); |
There was a problem hiding this comment.
You'll want to change the spelling to Invalid here.
|
|
||
| import org.junit.Test; | ||
|
|
||
| public class BookStoreTest{ |
There was a problem hiding this comment.
BookstoreTest please.
| public class BookStoreTest{ | ||
|
|
||
| @Test | ||
| public void OnlyASingleBook(){ |
There was a problem hiding this comment.
The camelCase Java rule would apply here.
| @Test | ||
| public void OnlyASingleBook(){ | ||
| Integer[] p = {1}; | ||
| List<Integer> books = new ArrayList<>(Arrays.asList(p)); |
There was a problem hiding this comment.
You can just pass the integers to Arrays.asList(). No need to declare and assign beforehand. EG:
List<Integer> books = new ArrayList<>(Arrays.asList(1));or
List<Integer> books = new ArrayList<>(Arrays.asList(1,2,3));| break; | ||
| case 5: | ||
| discountPercentage = 25; | ||
| break; |
There was a problem hiding this comment.
Sorry, I made an error! I thought the discount tiers were linear by groupSize, and they are not. My proposal and the file you uploaded both have a problem because of that. However, a solution like the PHP one you mentioned would work nicely:
private static double[] DISCOUNT_TIERS = {0,5,10,20,25}; //declare at class level
private double CostPerGroup(int groupSize) throws Exception{
if (groupSize < 1 || groupSize > 5){
throw new Exception("Invalid group size : " + groupSize );
}
return bookPrice * groupSize * (100 - DISCOUNT_TIERS[groupSize-1])/100;
}
matthewmorgan
left a comment
There was a problem hiding this comment.
@Twisti this is really looking terrific. There are two small changes you should make to adhere to Java conventions and track style.
Also we will need to resolve the merge conflict in config.json that was caused my merging another PR earlier. If you need help with this please let me know.
After that this will be ready to merge!
| private int bookPrice, maxGroupSize; | ||
| private double discountIncrement; | ||
| private List<Integer> books; | ||
| private static double[] discount_tiers = {0,5,10,20,25}; |
There was a problem hiding this comment.
Normally a constant would be ALL_CAPITALS like this. Most editors will recognize them as such and make them easier to spot in the code.
There was a problem hiding this comment.
I added static to constants BOOK_PRICE, MAX_GROUP_SIZE.
Also, I deleted the discountIncrement because it's useless now.
|
|
||
|
|
||
| if(books.size() == 0) | ||
| return priceSoFar; |
There was a problem hiding this comment.
Curly braces here please.
|
Beautiful! Thank you so much 🥇 |
I'll work on the bookstore exercise using C# version
https://github.com/exercism/x-common/tree/master/exercises/book-store