Skip to content

Palindrome Product Exercise #501

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 24 commits into from
Apr 27, 2018
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -767,6 +767,18 @@
"difficulty": 4,
"topics": []
},
{
"uuid": "8cdc3424-51da-4cae-a065-9982859d5b55",
"slug": "palindrome-products",
"core": false,
"unlocked_by": null,
"difficulty": 4,
"topics": [
"string comparison",
"calculation",
"structs"
]
},
{
"uuid": "0a33f3ac-cedd-4a40-a132-9d044b0e9977",
"slug": "poker",
Expand Down
6 changes: 6 additions & 0 deletions exercises/palindrome-products/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[package]
name = "palindrome-products"
version = "0.1.0"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When there exists canonical data, we use this version field to indicate when the problem was last synchronized to the canonical data. As such, we'd expect this to be "1.1.0", because that's the canonical data version.

authors = ["Kirill Meng <[email protected]>"]

[dependencies]
66 changes: 66 additions & 0 deletions exercises/palindrome-products/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Palindrome Products
Detect palindrome products in a given range.

A palindromic number is a number that remains the same when its digits are
reversed. For example, `121` is a palindromic number but `112` is not.

Given a range of numbers, find the largest and smallest palindromes which
are products of numbers within that range.

Your solution should return the largest and smallest palindromes, along with the
factors of each within the range. If the largest or smallest palindrome has more
than one pair of factors within the range, then return all the pairs.

## Example 1

Given the range `[1, 9]` (both inclusive)...

And given the list of all possible products within this range:
`[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 14, 16, 18, 15, 21, 24, 27, 20, 28, 32, 36, 25, 30, 35, 40, 45, 42, 48, 54, 49, 56, 63, 64, 72, 81]`

The palindrome products are all single digit numbers (in this case):
`[1, 2, 3, 4, 5, 6, 7, 8, 9]`

The smallest palindrome product is `1`. Its factors are `(1, 1)`.
The largest palindrome product is `9`. Its factors are `(1, 9)` and `(3, 3)`.

## Example 2

Given the range `[10, 99]` (both inclusive)...

The smallest palindrome product is `121`. Its factors are `(11, 11)`.
The largest palindrome product is `9009`. Its factors are `(91, 99)`.

## Rust Installation
Refer to the [exercism help page][help-page] for Rust installation and learning
resources.

## Writing the Code

Execute the tests with:

```bash
$ cargo test
```

All but the first test have been ignored. After you get the first test to
pass, remove the ignore flag (`#[ignore]`) from the next test and get the tests
to pass again. The test file is located in the `tests` directory. You can
also remove the ignore flag from all the tests to get them to run all at once
if you wish.

Make sure to read the [Modules](https://doc.rust-lang.org/book/second-edition/ch07-00-modules.html) chapter if you
haven't already, it will help you with organizing your files.

## Feedback, Issues, Pull Requests

The [exercism/rust](https://github.com/exercism/rust) repository on GitHub is the home for all of the Rust exercises. If you have feedback about an exercise, or want to help implement new exercises, head over there and create an issue. Members of the rust track team are happy to help!

If you want to know more about Exercism, take a look at the [contribution guide](https://github.com/exercism/docs/blob/master/contributing-to-language-tracks/README.md).

[help-page]: http://exercism.io/languages/rust
[modules]: https://doc.rust-lang.org/book/second-edition/ch07-00-modules.html
[cargo]: https://doc.rust-lang.org/book/second-edition/ch14-00-more-about-cargo.html

## Submitting Incomplete Solutions
It's possible to submit an incomplete solution so you can see how others have completed the exercise.
95 changes: 95 additions & 0 deletions exercises/palindrome-products/example.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
#[derive(Debug, PartialEq)]
pub enum Error{
Empty,
RangeFailure,
}

#[allow(dead_code)]
#[derive(Debug, PartialEq)]
pub struct ReturnVals{
pub result: i32,
pub factors: Vec<(i32, i32)>,
}

pub fn get_smallest_palindrome_product(min: i32, max: i32) -> Result<ReturnVals, Error>{
if min > max{
return Err(Error::RangeFailure);
}

let palindrome: Result<i32, Error> = get_smallest_palindrome(min, max);
if palindrome.is_err(){
return Err(palindrome.unwrap_err());
}
let p = palindrome.unwrap();
Ok(ReturnVals{
result: p,
factors: get_factors(p, min, max),
})
}

pub fn get_largest_palindrome_product(min: i32, max: i32) -> Result<ReturnVals, Error>{
if min > max{
return Err(Error::RangeFailure);
}

let palindrome: Result<i32, Error> = get_largest_palindrome(min, max);
if palindrome.is_err(){
return Err(palindrome.unwrap_err());
}
let p = palindrome.unwrap();
Ok(ReturnVals{
result: p,
factors: get_factors(p, min, max),
})
}

fn get_factors(n: i32, min: i32, max:i32)-> Vec<(i32, i32)>{
let mut factors = Vec::new();

for number in min .. max{
let div = n/number;
if n % number == 0 && div <= max && div >= min && !factors.contains(&(div, number)){
factors.push((number, n/number));
}
}
factors
}

fn get_smallest_palindrome(min: i32, max:i32)-> Result<i32, Error>{
let l:Vec<i32> = (min*min .. max*max).collect();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This approach isn't very efficient, particularly as the difference between min and max grows. You'd do better with a nested for loop:

for i in min..(max+1) {
    for j in i..(max + 1) {
        if is_palindrome(i*j) {
            ...
        }
    }
}

let filtered: Vec<i32> = l.iter()
.cloned()
.filter(|n| is_palindrome(*n) && has_factors(*n, min, max))
.collect::<Vec<i32>>();
if filtered.is_empty(){
return Err(Error::Empty);
} else{
Ok(*filtered.iter().min().unwrap())
}
}

fn get_largest_palindrome(min: i32, max:i32)-> Result<i32, Error>{
let l:Vec<i32> = (min*min .. max*max).collect();
let filtered: Vec<i32> = l.iter()
.cloned()
.filter(|n| is_palindrome(*n) && has_factors(*n, min, max))
.collect::<Vec<i32>>();
if filtered.is_empty(){
return Err(Error::Empty);
} else{
Ok(*filtered.iter().max().unwrap())
}

}

fn has_factors(n: i32, min:i32, max:i32)->bool{
let fac = get_factors(n, min, max);
!fac.is_empty()
}

fn is_palindrome(s: i32)->bool{
let s1 = s.to_string();
let s2 = s1.chars().rev().collect::<String>();

s1 == s2
}
17 changes: 17 additions & 0 deletions exercises/palindrome-products/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#[derive(Debug, PartialEq)]
pub enum Error{
Empty,
RangeFailure,
}
#[derive(Debug, PartialEq)]
pub struct ReturnVals{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be named Palindrome, which is much more descriptive than ReturnVals.

pub result: i32,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No negative number can be a palindrome, so let's use an unsigned type for the palindromes.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use u64, in fact, because it's easier than you might expect to overflow a u32. It's fine if min and max are u32, however; this helps reduce the chance of an overflow.

pub factors: Vec<(i32, i32)>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not obvious to me what value there is in returning the factors here. Best case, there exist certain implementations for which one can get the factorization more or less for free when generating the palindrome. Worst case, the student has to go back and factorize their solutions after generation, which is at best an orthogonal problem.

I see that the canonical data has them. There's no real explanation of why. I therefore propose that for the Rust track, we diverge from the canonical data by dropping the factors from the implementation and the tests.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we do this, we'd end up with a much nicer type for Palindrome:

pub type Palindrome = u64;

}

pub fn get_smallest_palindrome_product(_min: i32, _max:i32)->Result<ReturnVals, Error>{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not a huge fan of having get_smallest_palindrome_product and get_largest_palindrome_product as separate functions. This forces the machine to do extra work, which slows down the tests. Given that every test in the canonical data examines both the smallest and largest results, how about the following API:

pub struct Palindrome {
    pub value: i32,
    pub factors: Vec<(i32, i32)>,
}

pub type Palindromes = Vec<Palindrome>;

// in the case of an empty range or no results, we can just return an empty Vec, which simplifies the API
pub fn get_palindrome_products(min: i32, max: i32) -> Palindromes {
    unimplemented!("Find all palindromic numbers which are products of numbers in the inclusive range ({}..{})", min, max)
}

pub fn min(palindromes: &Palindromes) -> Option<Palindrome> {
    unimplemented!("Return the palindrome of minimal value from the supplied list: {:?}", palindromes)
}

pub fn max(palindromes: &Palindromes) -> Option<Palindrome> {
    unimplemented!("Return the palindrome of maximal value from the supplied list: {:?}", palindromes)
}

The tests could then be written in such a way that for any given test range, the computation is then performed only once.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per #476, we're avoiding the use of underscore-prefixed variables in favor of descriptive text in the unimplemented!() block.

unimplemented!();
}
pub fn get_largest_palindrome_product(_min: i32, _max: i32)->Result<ReturnVals, Error>{
unimplemented!();
}
84 changes: 84 additions & 0 deletions exercises/palindrome-products/tests/palindrome-products.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
extern crate palindrome_products;
use palindrome_products::*;

enum GET{
Smallest,
Largest,
}

fn test(e: GET, (min, max): (i32, i32))->Result<ReturnVals, Error>{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function appears not to do very much. One possibility would be to remove it entirely. Another would be to expand it into a macro which writes individually-ignorable tests from the input data. I find the second option much cooler, and am willing to help you work on the macro if you need help. See the perfect-numbers tests to see how that might look.

match e{
GET::Smallest => return get_smallest_palindrome_product(min, max),
GET::Largest => return get_largest_palindrome_product(min, max),
}
}
#[test]
fn smallest_palindrome_single_digits(){
assert_eq!(test(GET::Smallest, (1,9)), Ok(ReturnVals{result: 1, factors: vec!((1,1))}));
}

#[test]
#[ignore]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first test in the tests file should be the one not ignored. There are at least two ways to accomplish this:

  • move the single_digits test to the top of the file.
  • ignore single_digits and unignore empty_result_for_smallest_palindrome.

I do not have a preference between these options.

fn largest_palindrome_single_digits(){
assert_eq!(test(GET::Largest, (1,9)), Ok(ReturnVals{result: 9, factors: vec!((1,9), (3,3))}));
}

#[test]
#[ignore]
fn smallest_palindrome_double_digits(){
assert_eq!(test(GET::Smallest, (10,99)), Ok(ReturnVals{result: 121, factors: vec!((11,11))}))
}

#[test]
#[ignore]
fn largest_palindrome_double_digits(){
assert_eq!(test(GET::Largest, (10,99)), Ok(ReturnVals{result: 9009, factors: vec!((91,99))}))
}

#[test]
#[ignore]
fn smallest_palindrome_triple_digits(){
assert_eq!(test(GET::Smallest, (100,999)), Ok(ReturnVals{result: 10201, factors: vec!((101,101))}))
}

#[test]
#[ignore]
fn largest_palindrome_triple_digits(){
assert_eq!(test(GET::Largest, (100,999)), Ok(ReturnVals{result: 906609, factors: vec!((913,993))}))
}

#[test]
#[ignore]
fn smallest_palindrome_four_digits(){
assert_eq!(test(GET::Smallest, (1000,9999)), Ok(ReturnVals{result: 1002001, factors: vec!((1001,1001))}))
}

#[test]
#[ignore]
fn largest_palindrome_four_digits(){
assert_eq!(test(GET::Largest, (1000,9999)), Ok(ReturnVals{result: 99000099, factors: vec!((9901,9999))}))
}

#[test]
#[ignore]
fn empty_result_for_smallest_palindrome(){
assert_eq!(test(GET::Smallest, (1002,1003)), Err(Error::Empty));
}

#[test]
#[ignore]
fn empty_result_for_largest_palindrome(){
assert_eq!(test(GET::Largest, (15,15)), Err(Error::Empty));
}

#[test]
#[ignore]
fn error_smallest_palindrome_when_min_bt_max(){
assert_eq!(test(GET::Smallest, (1000,1)), Err(Error::RangeFailure));
}

#[test]
#[ignore]
fn error_largest_palindrome_when_min_bt_max(){
assert_eq!(test(GET::Largest, (2,1)), Err(Error::RangeFailure));
}