đź“° Exercism Rust - Clock

Posted on May 17, 2022 by Myoungjin Jeon
Tags: exercism, rust, clock

Clock - Instructions from Exercism.org

Implement a clock that handles times without dates.

You should be able to add and subtract minutes to it.

Two clocks that represent the same time should be equal to each other.

You will also need to implement .to_string() for the Clock struct. We will be using this to display the Clock’s state. You can either do it via implementing it directly or using the Display trait.

Did you implement .to_string() for the Clock struct?

If so, try implementing the Display trait for Clock instead.

Traits allow for a common way to implement functionality for various types.

For additional learning, consider how you might implement String::from for the Clock type. You don’t have to actually implement this—it’s redundant with Display, which is generally the better choice when the destination type is String – but it’s useful to have a few type-conversion traits in your toolkit.

My first Iteration(implementation)

This is my first working version. I didn’t know that there is rem_euclid() function, and I made my own similar function first. and I stored each property in a separate member variable, hours and minutes, respectively. Even though I could easily find that some other exercism user just stored in a single variable. (minutes only).

use std::fmt;

#[derive(PartialEq, Debug, Clone, Copy)]
pub struct Clock {
    hours: i32,
    minutes: i32,
}

const MINUTES_PER_HOUR: i32 = 60;
const HOURS_PER_DAY: i32 = 24;

pub fn clock_div_mod (val: i32, by: i32) -> (i32, i32) {
    let ( mut quot, mut modu ) = ( val / by, val % by );

    if modu < 0 {
        modu += by;
        quot -= 1;
    }

    ( quot, modu )
}

Basically, clock_div_mod will do wrapping down(floor) if the value is negative.

To display via std::fmt::Display trait, An implement for my Clock struct was required.

I think this structure – hours and minutes are stored separately – is good for quick displaying because hours and minutes are not required to calculated everytime.

// https://doc.rust-lang.org/rust-by-example/hello/print/print_display.html
impl fmt::Display for Clock {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(
            f,
            "{hours:0>2}:{minutes:0>2}",
            hours = &self.hours,
            minutes = &self.minutes
        )
    }
}

{hours:0>2} is working similar to below code in Perl (or c language)

printf( "%02d", 9 );

Clock has a constructor and method for add minutes:

impl Clock {
    pub fn new(hours: i32, minutes: i32) -> Self {
        let mut clock = Clock {
            hours : hours,
            minutes : minutes,
        };

        // clean up the hours and minutes
        clock.add_minutes( 0 )
    }

    pub fn add_minutes(&mut self, minutes: i32) -> Self {
        let ( additional_hours, new_minutes ) =
            clock_div_mod( self.minutes + minutes, MINUTES_PER_HOUR );
        let ( _, new_hours ) =
            clock_div_mod( self.hours + additional_hours,
                           HOURS_PER_DAY );

        self.hours   = new_hours;
        self.minutes = new_minutes;
        *self
    }
}

To Update or Not to Update

But when I look someones’ codes and realized that the person doesn’t care about updating member values when calling add_minutes. They just create a new instance of Clock. Unfortunately, the test code on this task doesn’t care about it is modified. but I’m wondering why we take &mut self if we don’t need to update the values?

I concluded that the spec doesn’t say anything about update original instance so it is not required to implement. However, generally, it is safer to keep most of things immutable. (and this task didn’t keep it even though mutability is not desired.)

implicit return without semi-colon(;)

This is something that I am familiar with. In perl language, we can return a value as exactly same as we can in rust. (there are more … raku, ruby .. but not python)

# this is a perl code not rust
sub return_some_clock_as_hash {
    { 'hours' => 16,
      'minutes' => 53,
    }
}

You can define even a constant value in the same way in perl.

In Rust, we cannot end the implicit statement with semicolon(;), On the contrary, we can even add a semi-colon in perl. (maybe in ruby as well?) as perl always return the last statement in a code block.

# perl code
sub HOURS_PER_DAY { 24 }

# or
sub HOURS_PER_DAY { 24; }

# both are working in Perl

BTW, there is a module for defining constant for perl.

credit: https://perldoc.perl.org/constant

# perl's first idiom:  There's more than one way to do it.
# ??: but please don't give me too much.
sub PI { 4 * atan2(1,1) }
# or
use constant PI => 4 * atan2(1,1)

Nevetheless, it is quite convenient way to return a value and we can make sure that there is no more code logically after the code without semicolon.

So if I put more code after *self, the compiler will report an error regarding to your syntax.

// .. snip ..
        self.hours   = new_hours;
        self.minutes = new_minutes;
        *self; // note: semi-colon added
        // and there is no return type
// .. snip ..

Will produce error message when compiling.

   Compiling clock v0.1.0 (/path/to/your/code/clock)
error[E0308]: mismatched types
  --> src/main.rs:54:52
   |
54 |     pub fn add_minutes(&mut self, minutes: i32) -> Self {
   |            -----------                             ^^^^ expected struct `Clock`, found `()`
   |            |
   |            implicitly returns `()` as its body has no tail or `return` expression
...
59 |         *self;
   |              - help: consider removing this semicolon

For more information about this error, try `rustc --explain E0308`.
error: could not compile `clock` due to previous error

My Forth Iteration

use std::fmt;

#[derive(PartialEq, Debug, Clone, Copy)]
pub struct Clock {
    hours: u8,
    minutes: u8,
}

And also I reduced the struct member size by using u8 for each member variable hours and minutes.

const MINUTES_PER_HOUR: i32 = 60;
const HOURS_PER_DAY: i32 = 24;

pub fn unsafe_clock_div_mod(val: i32, divider_: i32) -> (i32, i32) {
    // which doesn't check divider could be zero
    let divider = divider_ as i32;

    let (mut quot, mut modu) = (val / divider, val % divider);

    // below condition will not used in this implementation though ...
    if modu < 0 {
        modu += divider;
        quot -= 1;
    }
    (quot, modu)
}

// https://doc.rust-lang.org/rust-by-example/hello/print/print_display.html
impl fmt::Display for Clock {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(
            f,
            "{hours:0>2}:{minutes:0>2}",
            hours = &self.hours,
            minutes = &self.minutes
        )
    }
}

rem_euclid

I realized that second call of clock_div_mod creats an unused value along with new_hours value. so I modified in my forth iteration. so I used rem_euclid for the case.

And I add helper member function which is called to_my_clock for new and add_minutes.

impl Clock {
    fn to_my_clock(hours: i32, minutes: i32) -> Self {
        let total_minutes =
            (hours * MINUTES_PER_HOUR + minutes).rem_euclid(HOURS_PER_DAY * MINUTES_PER_HOUR);

        let (new_hours, new_minutes) = unsafe_clock_div_mod(total_minutes, MINUTES_PER_HOUR);

        Clock {
            hours: new_hours as u8,
            minutes: new_minutes as u8,
        }
    }

    pub fn new(hours: i32, minutes: i32) -> Self {
        Clock::to_my_clock(hours, minutes)
    }

    pub fn add_minutes(&mut self, minutes: i32) -> Self {
        let new_clock = Clock::to_my_clock(self.hours as i32, self.minutes as i32 + minutes);

        // update values
        *self = new_clock;
        *self
    }
}

I am still unsure I need to separate the values into hours and minutes, but I guess it is depends on the situation. I could only guess that If we modify the value less and display more, it is better idea to separate them. Otherwise, we can keep in a single member variable.

Wrapping Up

In this task, I realized that:

Thank you

Thank you for reading !! I am still very confused with Rust language, but it seems worth learning!!