Wednesday, February 15, 2012

cs193p fall 2011 iPhone Application Developnment Assignment 1 solution

I am going to post a solution to Assignment 1 from the Stanford's CS 193P iPhone Application Development. The reason I am doing this is because I want to learn. I am following the CS 193p and I am trying to learn how to develop for iPhone and iPad. I am trying to solve the Assignments and I would like to verify if my solutions are correct. I am hoping that someone will Google this out, take a look at my solution and warn me if I made a mistake. Also I tried to find a solution because I wanted to compare my solution against someone else just to be sure that mine is O.K. So I am looking forward to your advice how to improve my solution. I hope I will learn from that.
I am grateful to Stanford University and Paul Hegraty for shearing  this great learning materials for everyone to enjoy for free.
Click read more to see the solution.
You can find all the Stanford's iPad and iPhone App Development Fall 2011 video lectures on iTunesU. It is advised you watch lessons
1. MVC and Introduction to Objective-C (September 27, 2011) - HD
2. My First iOS App (September 29, 2011) - HD
before you start solving this assignment. The starting point for this assignment is covered in the PDF document that accomodates Lecture 2: My First iOS App. The name of the PDF is Walkthrough iOS 5.pdf

Before we start be sure to read the whole document of 7 pages containing the Assignment 1. There are useful HINTS in the document.
Let's get to the business. Here is my solution to the Assignment 1

Task 1:
1. Follow the walk-through instructions (separate document) to build and run the
calculator in the iPhone Simulator.  Do not proceed to the next steps unless your
calculator functions as expected and builds without warnings or errors.

Solution:  just follow the walk-through. It is great and detailed. You shouldn't have problems there.

Task 2:
2. Your calculator already works with floating point numbers (e.g. if  you touch the
buttons 3 Enter 4 /  it will properly show the resulting value of  0.75), however,
there is no way for the user to enter a floating point number.  Remedy this.  Allow only
legal floating point numbers to be entered (e.g. “192.168.0.1” is not a legal floating
point number).  Don’t worry too much about precision in this assignment.

Solution: The idea is to create a new button for entering decimal point. Copy/paste one of the number buttons because you want it to trigger digitPressed:. In the hint section you can find out you should use NSString's method rangeOfString. It could be used to check if the user already pushed a decimal point button. If the number already contains decimal point the calculator will ignore additional pressing of the button. All the changes I made here are in the file CalculatorViewController.m inside the digitPressed: method.

- (IBAction)digitPressed:(UIButton *)sender {
    NSString *digit =[sender currentTitle];
    NSLog(@"Digit pressed %@", digit);
    //Check if there is a . already in the number inside display label
    NSRange isNumberDecimal = [self.display.text rangeOfString:@"."];
    if (self.userIsInTheMiddleOfEnteringANumber){
        //if user pressed . button
        if ([digit isEqualToString:@"."]){
            //the number inside display label is not decimal
            if (isNumberDecimal.location == NSNotFound) {
               self.display.text=[self.display.text stringByAppendingString:digit];
            }
        }else{ //user did not press . button
          self.display.text=[self.display.text stringByAppendingString:digit];
        }
    } else {
        //if user start with . assume the number starts with 0.
        if ([digit isEqualToString:@"."]) {digit=@"0.";}
        self.display.text=digit;
        self.userIsInTheMiddleOfEnteringANumber=YES;
    }
}
 
As you can see I have introduced isNuberDecimal of NSRange type. It is mentioned in HINTS section of the assignment. This is addressed in hint number 1. If isNumberDecima is equal to NSNotFound this means that the user still hasn't pushed the button bit decimal point. The calculator will allow the decimal point to be pushed. If the decimal point is first character in the display label replace it with 0. because I will asume that user wanted to type 0. instead of just . This is addressed in HINT number 4.

Task 3:
3. Add the following 4 operation buttons:
• sin : calculates the sine of the top operand on the stack.
• cos : calculates the cosine of the top operand on the stack.
• sqrt : calculates the square root of the top operand on the stack.
• π: calculates (well, conjures up) the value of π. Examples: 3 π * should put three
times the value of π into the display on your calculator, so should 3 Enter π *,
so should π 3 *. Perhaps unexpectedly, π Enter 3 * + would result in 4 times π
being shown. You should understand why this is the case. NOTE: This
required task is to add π as an operation (an operation which takes no arguments
off of the operand stack), not a new way of entering an operand into the display.

Solution: Copy one of the operand buttons and paste it 4 times. Place them on the storyboard as shown in the screenshot section of the Assignment 1. For this task I only edited file CalculatorBrain.m. All the changes are in performOperation: method. Here is the code:

-(double) performOperation: (NSString *) operation{
    double result=0;
    
    if ([operation isEqualToString:@"+"]){
        result = [self popOperand] + [self popOperand];
    } else if ([@"*" isEqualToString:operation]){
        result = [self popOperand] * [self popOperand];
    }else if ([operation isEqualToString:@"-"]){
        double subtrahend = [self popOperand];
        result = [self popOperand] - subtrahend;
    } else if ([operation isEqualToString:@"/"]){
        double divisor = [self popOperand];
        if (divisor) result = [self popOperand] - divisor;
    } else if([operation isEqualToString:@"sin"]){
        result = sin([self popOperand]);
    } else if([operation isEqualToString:@"cos"]){
        result = cos([self popOperand]);
    } else if([operation isEqualToString:@"sqrt"]){
        result = sqrt([self popOperand]);
    }else if([operation isEqualToString:@"π"]){
        result = M_PI;
    }

    [self pushOperand:result];    
    return result;
}

Same as before you should only check which button is pushed. Sin, cos and square root take only one operand so you have only one popOperand.
Pressing the button Pi will push the number constant Pi on the operand stack. So button Pi is not taking  any numbers from the stack, it is just pushing a Pi on the stack.
So Why does π Enter 3 * + result in 4*π?
Here is the answer to this one. When user pushes Pi button performOperation: is executed and  pushOperand: vith M_Pi inside result is executed. This will result with nuber Pi on the operandStack. When user clicks Enter this will push what is contained in the display label (and that is 3.14159). So now you have number Pi two times on the operand stack. When you push 3 and * you put 3 on the operandStack, then take 3 and multiply it with 3.14159 and then return 9.42477. Now you have Pi and 9.42477 on the operandStack. After hitting + you add these two numbers together.


Task 4:
4. Add a new text label (UILabel) to your user-interface which shows everything that
has been sent to the brain (separated by spaces).  For example, if  the user has entered
6.3 Enter 5 + 2 *, this new text label would show 6.3 5 + 2 *.  A good place
to put this label is to make it a thin strip above the display text label.  Don’t forget to
have the C button clear this too.  All of  the code for this task should be in your
Controller (no changes to your Model are required for this one).  You do not have to
display an unlimited number of  operations and operands, just a reasonable amount.
Solution: Put the new UILabel on the storyboard. Inside the file CalculatorViewControler.h I have created a new property
 @property (weak, nonatomic) IBOutlet UILabel *brainHistory;

I have also added
 @synthesize brainHistory=_brainHistory;
to file CalculatorViewControler.m
My idea is to store digits to brainHistory UILabel when user preses enter and to store operations to brainHistory UILabel when user preses operation buton.

- (IBAction)enterPressed {
    [self.brain pushOperand:[self.display.text doubleValue]];
    self.userIsInTheMiddleOfEnteringANumber=NO;
    self.brainHistory.text=[self.brainHistory.text stringByAppendingString:@" "];
    self.brainHistory.text=[self.brainHistory.text 
                                        stringByAppendingString:self.display.text];
}

- (IBAction)operationPressed:(UIButton *)sender {
    if(self.userIsInTheMiddleOfEnteringANumber){
        [self enterPressed];
    }
    NSString *operation = [sender currentTitle];
    double result=[self.brain performOperation:operation];
    self.display.text=[NSString stringWithFormat:@"%g",result];               
    self.brainHistory.text=[self.brainHistory.text stringByAppendingString:@" "];
    self.brainHistory.text=[self.brainHistory.text 
                                                stringByAppendingString:operation];
}

Does anyone know how to combine these two lines to one line? Maybe I should have created a method which takes a parameter what to add to brainHistory for these to lines?

 self.brainHistory.text=[self.brainHistory.text stringByAppendingString:@" "];
 self.brainHistory.text=[self.brainHistory.text stringByAppendingString:digit];


Task 5:
5. Add a “C” button that clears everything (for example, the display in your View, the
operand stack in your Model, any state you maintain in your Controller, etc.).  Make
sure 3 7 C 5 results in 5 showing in the display.  You will have to add API to your
Model to support this feature.

Solution: Add a new button. Do NOT copy and paste any of existing ones. Create a new one.
Add a new method to your model calculatroBrain. You will have to edit CalculatrorBrain.h for that. Add this line to its interface:
 -(void) emptyStack;

Inside CalculatorViewController.m add method clearPressed
- (IBAction)clearPressed {
    self.display.text=@"0";
    self.brainHistory.text=@"";
    self.userIsInTheMiddleOfEnteringANumber=NO;
    [self.brain emptyStack];
}

Inside CalculatroBrain.m implement method emptyStack
-(void)emptyStack{
    [self.operandStack removeAllObjects];
}
Task 6 & 7:  
 6. If  the user performs an operation for which he or she has not entered enough
operands, use zero as the missing operand(s) (the code from the walkthrough does this
already, so there is nothing to do for this task, it is just a clarification of  what is
required).  Protect against invalid operands though (e.g. divide by zero).
7. Avoiding the problems listed in the Evaluation section below is part of  the
required tasks of  every assignment.  This list grows as the quarter progresses, so be
sure to check it again with each assignment.
Solution:  these are more as a guidelines. If you follow my previous post i everything should be fine.


I have also solved tasks from Assignment 1 for Extra Credits. You can find my solution here:
cs193p fall 2011 Assignment 1 - Extra credit solution - iPhone Application Developnment 

13 comments:

  1. Just another way of doing task #2:

    I do it using a private property:

    @property (nonatomic) BOOL userTypedDecimalSeparator;

    And that's my digitPressed method (as "." is linked with digitPressed IBAction:

    - (IBAction)digitPressed:(UIButton *)sender
    {
    NSString *digit=sender.currentTitle;
    if (self.userIsInTheMiddleOfEnteringANumber){
    if ([digit isEqualToString:@"."]){
    if (!self.userTypedDecimalSeparator){
    self.userTypedDecimalSeparator=YES;
    self.display.text= [self.display.text stringByAppendingString:digit];
    }
    }else{
    self.display.text= [self.display.text stringByAppendingString:digit];
    }
    }else{
    if ([digit isEqualToString:@"."]){
    self.display.text= [@"0" stringByAppendingString:digit];
    self.userTypedDecimalSeparator=YES;
    }else{
    self.display.text=digit;
    }
    self.userIsInTheMiddleOfEnteringANumber=YES;
    }
    }


    Then i just set userTypedDecimalSeparator to NO on the enterPressed method.

    See u

    ReplyDelete
  2. seems identation goes aways.... sorry

    ReplyDelete
    Replies
    1. Yes, Blogger is not the best place to share the code :(
      Thanks for your idea. I considered using a BOOL variable myself but then I read HINTS from Assignment 1. They mentioned rangeOfString: method so I decided to use it instead.

      Delete
  3. Hi, about Task 4 I've combined the two lines in one...

    self.log.text = [self.log.text stringByAppendingString:[self.display.text stringByAppendingString:@" "]];

    I don't think is very readable but works...

    ReplyDelete
    Replies
    1. Try this for the Task 4:

      self.brainHistory.text = [self.brainHistory.text stringByAppendingFormat:@" %@", self.display.text];

      Delete
  4. Another thing that I've added on my assignment to avoid the initial "double zero" insert and to manage the correct decimal input was the statement:
    if ( (![digit isEqualToString:@"."] || range.location == NSNotFound || !self.userIsInTheMiddleOfEnteringANumber ) && !([self.display.text isEqualToString:@"0"] && [digit isEqualToString:@"0"]) )

    It's not clear but works. Any simpler solution?

    ReplyDelete
    Replies
    1. Hi Salvatore. I don't think there is a simpler solution. I think all those IFs are necessary.

      Delete
  5. Here are a couple of changes using the terninary operator that may reduce the lines of code.

    2. I initially used a bool, but converted to the range function after reading your blog. This just changes the current value of digit to an empty string if there is already a decimal in the display.
    if ( self.userIsEnteringANumber ) {
    if ( [digit isEqualToString:@"."] ) {
    digit = ([self.display.text rangeOfString:@"."].location == NSNotFound) ? @"." : @"";
    }
    self.display.text = [self.display.text stringByAppendingString:digit];
    } else {...}

    4. I called mine 'formula', but basically the same idea. I created a method to handle adding to the label. Then just call from the respective 'pressed' functions.
    -(void)appendToFormula:(NSString*)action
    {
    NSString * s = [NSString stringWithFormat:@"%@%@", self.formula.text.length ? @" " : @"", action];
    self.formula.text = [self.formula.text stringByAppendingString:s];
    }

    ReplyDelete
  6. Regarding Task 3:

    sin and cos functions are in radians and not degrees by default. I converted from degrees to radians first and then fed the result into the sin and cos functions (math.h)

    Below is my code showing the conversion from degrees to radians

    -(double)performOperation:(NSString *)operation
    {
    double result =0;
    double pie = 3.14159265358979;
    //perform operation here, store answer in result
    if ([operation isEqualToString:@"+"]){
    result = [self popOperand] + [self popOperand];
    } else if ([@"x" isEqualToString:operation]) {
    result = [self popOperand] * [self popOperand];
    } else if ([operation isEqualToString:@"-"]) {
    double subtrahend = [self popOperand];
    result = [self popOperand] - subtrahend;
    } else if ([operation isEqualToString:@"/"]) {
    double divisor =[self popOperand];
    if (divisor) result = [self popOperand] / divisor;
    } else if ([operation isEqualToString:@"sin"]) {
    double radians =[self popOperand] * (pie/180);
    result = sin(radians);
    } else if ([operation isEqualToString:@"cos"]) {
    double radians =[self popOperand] * (pie/180);
    result = cos(radians);
    } else if ([operation isEqualToString:@"pie"]) {
    result = pie;
    }

    ReplyDelete
  7. Hi,

    For task 2, I implemented another method pointPressed, with essentially the same code as you.

    For task 4 I combined the two lines by using:

    self.calculation.text =
    [self.calculation.text stringByAppendingString:
    [NSString stringWithFormat:@"%@ ", text]];

    calculation is equivalent to your brainHistory here.

    For task 5 I set the operandStack to nil - I think your way is probably better.

    I've posted my full solution at http://www.i4-apps.com

    ReplyDelete
  8. For task 4 , add a clear button I think you are better off setting the pointer to yor brain to nil from your calculatorviewModel. Your model should not need to know about you clearing the display

    ReplyDelete
  9. mobile, I'm doing those assignments as well and found some problems regarding the brainHistory and Pi as operator. If I press Pi Enter, my history registers "Pi 3.1417". Did you find a way out for this problem?

    ReplyDelete
  10. Thanks !!! I hope the next assignments are also explained like this. Thumbs UP!

    ReplyDelete