Bang for Your Buck: Exercise

Write a point-free function to get the 3 top-rated meals <= a given price.

Given a maximum price and menu, return the 3 top-rated meals for that price or less.
Usage

  1. // top 3 meals for $12 or less
  2. const best3Meals = getTop3MealsFor(12, menu);
  3. /*
  4. [{
  5. name: 'Lamb Gyro',
  6. price: 11.86,
  7. rating: 4.9
  8. }, {
  9. name: 'House Salad',
  10. price: 9.00,
  11. rating: 4.65
  12. }, {
  13. name: 'Gigantus Fries',
  14. price: 11.86,
  15. rating: 4.5
  16. }]
  17. */

Your solution must be point-free.
Notice how the menu parameter’s supplied last, aligning with Ramda’s “data-last” pattern. This lets you compose getTop3MealsFor with other functions to manipulate menu in different ways.

  1. const firstPerfectMeal = pipe(
  2. getTop3MealsFor(20),
  3. filter(
  4. both(isVegetarian, isLactoseFree)
  5. ),
  6. head
  7. );

This finds the best $20 or less vegetarian/lactose-free meal. All it needs is a menu parameter and off it goes!

menu.js

  1. export default [{
  2. name: 'Lamb Gyro',
  3. price: 11.86,
  4. rating: 3.31
  5. }, {
  6. name: 'Chicken Gyro',
  7. price: 11.48,
  8. rating: 3.84
  9. }, {
  10. name: 'Mixed Gyro',
  11. price: 11.69,
  12. rating: 4.81
  13. }, {
  14. name: 'Jumbo Lamb',
  15. price: 12.31,
  16. rating: 2.32
  17. }, {
  18. name: 'Lamb Kebab',
  19. price: 8.85,
  20. rating: 4.82
  21. }, {
  22. name: 'Jumbo Mixed',
  23. price: 14.96,
  24. rating: 3.37
  25. }, {
  26. name: 'Chicken Wings',
  27. price: 11.17,
  28. rating: 3.56
  29. }, {
  30. name: 'Biggie Fries',
  31. price: 14.36,
  32. rating: 3.86
  33. }, {
  34. name: 'Medium Fries',
  35. price: 10.36,
  36. rating: 4.96
  37. }, {
  38. name: 'Medium Curly Fries',
  39. price: 10.37,
  40. rating: 2.52
  41. }, {
  42. name: 'Biggie Curly Fries',
  43. price: 15.07,
  44. rating: 3.73
  45. }, {
  46. name: 'House Salad',
  47. price: 11.39,
  48. rating: 4.86
  49. }, {
  50. name: 'Apartment Salad',
  51. price: 14.38,
  52. rating: 2.80
  53. }, {
  54. name: 'Steak',
  55. price: 12.50,
  56. rating: 4.06
  57. }, {
  58. name: 'Shrimp Gyro',
  59. price: 14.57,
  60. rating: 3.48
  61. }];

Review #

Let’s review the steps

  1. Get everything maxPrice or less
  2. Sort by rating
  3. Get the top 3

Here’s a vanilla solution

  1. import menu from './menu';
  2. const getTop3MealsFor = (maxPrice, menu) => menu
  3. .filter((item) => item.price <= maxPrice)
  4. .sort((a, b) => b.rating - a.rating)
  5. .slice(0, 3);
  6. const result = getTop3MealsFor(12, menu);
  7. console.log({ result });

Ramda has slice and sort functions. Using them lets us pipe the entire function to make it point-free.

  1. import { pipe, slice, sort } from 'ramda';
  2. import menu from './menu';
  3. const getTop3MealsFor = pipe(
  4. (maxPrice, menu) => menu.filter((item) => item.price <= maxPrice),
  5. sort((a, b) => b.rating - a.rating),
  6. slice(0, 3)
  7. );
  8. const result = getTop3MealsFor(12, menu);
  9. console.log({ result });

We can point-free up filter’s predicate function using propSatisfies. It tests an object’s property against a function to return true or false.

  1. import { gte, pipe, propSatisfies, slice, sort } from 'ramda';
  2. import menu from './menu';
  3. const getTop3MealsFor = pipe(
  4. (maxPrice, menu) => menu.filter(propSatisfies(gte(maxPrice), 'price')),
  5. sort((a, b) => b.rating - a.rating),
  6. slice(0, 3)
  7. );
  8. const result = getTop3MealsFor(12, menu);
  9. console.log({ result });

I personally wouldn’t do that, but it’s fun to experiment.

Comparators #

I would, however, play around with comparators. These are functions that compare data for sorting.
Ramda’s descend function is a descending comparator, meaning a function that sorts entries from biggest to smallest. The flip side would be R.ascend, an ascending comparator.
We can break the sorting logic into a function

  1. const byPrice = descend(prop('rating'));

Then use it like so

  1. sort(byPrice);

It reads like a sentence!

  1. import { descend, gte, pipe, prop, propSatisfies, slice, sort } from 'ramda';
  2. import menu from './menu';
  3. const byPrice = descend(prop('rating'));
  4. const getTop3MealsFor = pipe(
  5. (maxPrice, menu) => menu.filter(propSatisfies(gte(maxPrice), 'price')),
  6. sort(byPrice),
  7. slice(0, 3)
  8. );
  9. const result = getTop3MealsFor(12, menu);
  10. console.log({ result });

https://ramdajs.com/docs/#slice

Number → Number → [a] → [a]
Number → Number → String → String
Parameters
Added in v0.1.4
Returns the elements of the given list or string (or object with a slice method) from fromIndex (inclusive) to toIndex (exclusive).
Dispatches to the slice method of the third argument, if present.

  1. R.slice(1, 3, ['a', 'b', 'c', 'd']); //=> ['b', 'c']
  2. R.slice(1, Infinity, ['a', 'b', 'c', 'd']); //=> ['b', 'c', 'd']
  3. R.slice(0, -1, ['a', 'b', 'c', 'd']); //=> ['a', 'b', 'c']
  4. R.slice(-3, -1, ['a', 'b', 'c', 'd']); //=> ['b', 'c']
  5. R.slice(0, 3, 'ramda'); //=> 'ram'