2

I'm learning AngularJS, trying to make a simple calculator, and I'm trying to condense this if/else if statement to 1-2 lines, using the Javascript logical Operators (&&, ||, !)

Given this example, how could I reduce it? (if you don't understand $scope, ignore it. Its basically a view. So when someone clicks 9, the calculator will display 9 on the answer screen)

$scope.setOperand = function (operandEntered) {

    if ($scope.leftOperand === null) {
        $scope.leftOperand = operandEntered;
    } 
    else if ($scope.operator === null) {
        $scope.leftOperand = $scope.leftOperand + operandEntered;
    } 
    else if ($scope.rightOperand === null) {
        $scope.rightOperand = operandEntered;
    } 
    else if ($scope.answer === null) {
        $scope.rightOperand = $scope.rightOperand + operandEntered;
    }
 };
Dalorzo
  • 19,834
  • 7
  • 55
  • 102
T Evon
  • 43
  • 8
  • 6
    Why you want to do that? It becomes harder to read and to maintain. There's absolutely no benefit in writing the shortest code possible because you "can". Btw. in the real world code gets minified and optimized by tools like uglify, so you should write your code in the most readable way possible. – cyr_x Jun 07 '17 at 23:40
  • I don't know i understand the logic of the code. It's easy enough to get it to two lines, simply remove whitespace until it fits all on two lines. – Jared Farrish Jun 08 '17 at 01:13

7 Answers7

1

One can always (try to) be clever, but abstraction doesn't always pay off when the code becomes longer and not more reusable. KISS.

But if you want to go for it, I'd choose

function addTo(property, x, operand) { // "x" for lack of a more meaningful name
    const shouldAdd = $scope[property] === null || $scope[x] === null;
    if (shouldAdd)
        $scope[property] += operand; // assuming "operand" is a number
// or   $scope[property] = ($scope[property] || "") + operand; // when it's a string
    return shouldAdd;
}
$scope.setOperand = function (operandEntered) {
    addTo("leftOperand", "operator", operandEntered) || addTo("rightOperand", "answer", operandEntered);
 };

If you care more about conciseness than about readability, you can even shorten the helper to

function addTo(property, x, operand) {
    return ($scope[property] === null || $scope[x] === null) && (($scope[property] += operand), true);
}
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • Do you mind looking at my answer? I'm not sure I matched what they're doing, although i tried to work it out (mainly the `$scope.answer` bit). – Jared Farrish Jun 08 '17 at 02:33
  • @JaredFarrish Yeah, those `answer` and `operator` bits I had problems with as well. Not sure what's the real intention of the code. – Bergi Jun 08 '17 at 02:39
  • intents were to have a calc that sets a left operand, an operator, then an right operand. sorry for the misclarifacation. I had a getAnswer method that made a $scope.answer. i posted all of my code further below in the answer section to make more sense. The operator also had a setOperator method that makes a $scope.operator. These are great questions! thank you so much! it hleps to know what i need to post in the future – T Evon Jun 08 '17 at 22:01
1

TLDR;

Making all the same assumptions as below, this will work and of course is dead simple:

$scope.setOperand = function (operand) {
    var prop = $scope['leftOperand'] === null ? 'leftOperand' : 'rightOperand';
    $scope[prop] = +$scope[prop] + operand;
};

The key is this part: +$scope[prop] That casts null to 0, so you end up adding it to one side or the other, if it's null or has a value (which is what the logic seems to do). Bergi pointed out that null and null for both values isn't handled, but I'd say the calculation should be done elsewhere:

$scope.calc = function(){
    return eval(
        +$scope.leftOperand         // Cast if null to 0
        + ($scope.operator || '+')  // Default to add if null
        +$scope.rightOperand‌        // Cast if null to 0
    );
};

Assuming you have one left/right operand (and you're not trying to do multiple operations):

var $scope = {
    operator: '-',
    answer: null,
    leftOperand: null,
    rightOperand: 3,
};

We can start with:

$scope.setOperand = function (operand) {
    var prop = ['leftOperand','rightOperand'].reduce(function(t, k) {
        return $scope[k] === null ? k : t;
    });
    $scope[prop] = +$scope[prop] + operand;
};

https://jsfiddle.net/w89dLrqw/

Which is four lines. We can remove one line with a little hijinks:

$scope.setOperand = function (operand) {
    [['leftOperand','rightOperand'].reduce(function(t, k) {
        return $scope[k] === null ? k : t;
    })].map(function(prop){$scope[prop] = +$scope[prop] + operand});
};

https://jsfiddle.net/b63x7aag/

Or, if you will:

$scope.setOperand = function (operand) {
    [['leftOperand','rightOperand'].reduce(function(t, k) {return $scope[k] === null ? k : t;})]
    .map(function(prop){$scope[prop] = +$scope[prop] + operand});
};

And another one (props to @bergi):

$scope.setOperand = function (operand) {
    (function(prop){$scope[prop] = +$scope[prop] + operand})
    (['leftOperand','rightOperand'].reduce(function(t, k){return !+$scope[k] ? k : t}));
};

https://jsfiddle.net/mh1bvhcj/1/

That last two look minified, and that last one runs "upside-down". I can't see how this is useful to write it this way just so it takes up very little horizontal space.

What I don't understand is what the else if ($scope.answer === null) is in there for, since having answer === null doesn't seem like it would affect the operands as far as I can tell. So this may or may not work, depending on what that's about.

Jared Farrish
  • 48,585
  • 17
  • 95
  • 104
  • Interesting. It doesn't seem to cover the case where nothing is assigned to anything at all, though. Also I'd recommend a dead simple IIFE instead of `[…].map(…)` – Bergi Jun 08 '17 at 02:38
  • Hmm. Wouldn't that be part of the evaluation operation? Like `$scope.eval = function(){return eval(+$scope.leftOperand+$scope.operator+$scope.rightOperand)};`? – Jared Farrish Jun 08 '17 at 02:43
  • I don't know that I can jam it all on two or three lines with an IIFE. – Jared Farrish Jun 08 '17 at 02:49
  • Just do `(function(prop){…})(['leftOperand','rightOperand'].reduce(…));` – Bergi Jun 08 '17 at 02:54
  • You can basically insert linebreaks arbitrarily at any parenthesis or brace, to get the number of lines and formatting feel that you desire. I didn't mean it to be a one-liner :-) – Bergi Jun 08 '17 at 03:03
  • Yes, but the OP did. Part of what I was trying to show was that it's absurd to reduce it down to so few lines that it becomes unreadable. Or at least counter-productive. – Jared Farrish Jun 08 '17 at 03:05
  • correct, yeah i'm not using multiple operands, just a left and right, then eventually i want the answer to be the leftOperand – T Evon Jun 08 '17 at 20:39
  • I think your code doesn't seem to implement the logic correctly - you are not testing `$scope.operator` properly. – NetMage Jun 08 '17 at 21:45
  • @NetMage "properly" is not the operative term (no pun intended); "not at all" would be right. – Jared Farrish Jun 08 '17 at 21:48
  • @NetMage - Give me an example of it not working with inputs and I'll take a look. – Jared Farrish Jun 08 '17 at 21:50
  • @JaredFarrish Consider: https://jsfiddle.net/mh1bvhcj/ - your code never updates `rigthOperand` but after `operator` is set, it should. – NetMage Jun 08 '17 at 21:53
  • @NetMage - I was missing a negation on the check for the left/right, updated the fiddle in the answer. – Jared Farrish Jun 08 '17 at 21:58
  • @JaredFarrish I still see Left: 18, Right: 6 instead of Left: 12, Right: 12 on the second run (Chrome doesn't seem to like the first run?). I don't like jsfiddle because it isn't for pure Chrome, but I haven't seen much better anywhere. – NetMage Jun 08 '17 at 23:32
  • I don't see any reason it's got to be balanced. In any event, the OP seems to indicate it's only for a single pass. – Jared Farrish Jun 08 '17 at 23:36
0

I reconsidered - you can make it a bit better in one way, and worse in another.

For each expression like this:

$scope.leftOperand = $scope.leftOperand + operandEntered;

you can use the assignment operator:

$scope.leftOperand += operandEntered;

to shorten it slightly.

I have re-re-re-considered. Assuming you are actually doing string concatenation to build numbers from entered digits, I think this is what you want (I did skip over the 'answer' condition as something that shouldn't happen).

$scope.setOperand = function (operandEntered) {
    var prop = ($scope.operator === null) ? 'leftOperand' : 'rightOperand';
    $scope[prop] = ($scope[prop] === null) ? operandEntered : $scope[prop]+operandEntered;
};
NetMage
  • 26,163
  • 3
  • 34
  • 55
  • Sure you could do that like: `var left = $scope.leftOperand*!$scope.operator; [$scope.leftOperand, $scope.rightOperand] = [left + operandEntered, !left && !$scope.answer*$scope.rightOperand + operandEntered || null]` – cyr_x Jun 07 '17 at 23:47
  • I wouldn't assume `!` is a good replacement for `$scope.answer === null` - what if `$scope.answer` is `0`? – NetMage Jun 07 '17 at 23:50
  • Yep, you're right should do `($scope.answer === null)*...`. You can easily make it more type safe and/or maybe smaller with bitwise operations. But as i said in my comment, no benefit to do that. – cyr_x Jun 07 '17 at 23:57
  • @TEvon I changed my answer based on re-interpreting your question. – NetMage Jun 08 '17 at 21:51
  • yeah thats a great point. i changed it to an empty string. i updated my code and its working above, i posted an answer and updated it with the condensed setOperand() function so it doesn't have if, else if, else if, else if... if you see any other tweaks or modifications let me know – T Evon Jun 08 '17 at 21:57
0

Well there is this ppossibility where:

$scope.setOperand=(operandEntered)=>{
    $scope.leftOperand=($scope.leftOperand===null)? // if
        operandEntered
        : // else if v
        ($scope.operator===null)?
            $scope.leftOperand+operandEntered
            : // else v
            (()=>{
                $scope.rightOperand=($scope.rightOperand===null)? // if
                    operandEntered
                    : // else if v
                    ($scope.answer===null)?
                        $scope.rightOperand+operandEntered
                        : // else v
                        $scope.rightOperand;
                return $scope.leftOperand;
            })();
};

Which then shortens to this:

$scope.setOperand=(operandEntered)=>{$scope.leftOperand=($scope.leftOperand===null)?operandEntered:($scope.operator===null)?$scope.leftOperand+operandEntered:(()=>{$scope.rightOperand=($scope.rightOperand===null)?operandEntered:($scope.answer===null)?$scope.rightOperand+operandEntered:$scope.rightOperand;return $scope.leftOperand;})();};

Which does exactly as you asks, and shortens each if/else if statement.

0

Given the updated code that the OP has provided, a single statement will now suffice for $scope.setOperand(), as follows:

$scope.setOperand = function (operandEntered) {
          $scope.operator
          ? $scope.rightOperand += operandEntered 
          : $scope.leftOperand += operandEntered;
    };

The simple ternary addresses the original concern of the OP; see deno here.

I was unable to get the "C" button to clear the display which may be due to the way things are configured at codepen.io where location.reload() is perhaps not permissible there. So, I recoded that button, as follows:

$scope.setClear = function (a) {

          $scope.leftOperand = "";
          $scope.operator = "";
          $scope.rightOperand = "";
          $scope.answer = "";

    };

See demo here.

Now the displays clears and without any wait for a page reload. Another change I made is as follows:

if (answer % 1 !== 0)

This ensures that only floating point values display as decimals. Now 1 + 2 equals 3 instead of 3.00. (Consulted this resource: How do I check that a number is float or integer?)

slevy1
  • 3,797
  • 2
  • 27
  • 33
  • 1
    It's not two line. LOL Just kidding. So you're saying this is a classic computer science class question? I had interpreted the `if` block logic as _set an operand on a null field, or, if not null, add to one side, left first_. – Jared Farrish Jun 08 '17 at 12:27
  • Were you able to get the OP's multiple if-statement code to work as a calculator? If yes, then your interpretation may be correct. I just recall that when I was a newbie to another language, namely C, my prof. requested that the students implement a reverse Polish notation calculator with the twist that it had to execute in a Windows environment. The OP apparently has another kind of calculator in mind judging from the post below showing the JS and HTML.. – slevy1 Jun 08 '17 at 19:51
0

----------------------------UPDATED------------WORKING CODE-------------------

** HTML **

<!DOCTYPE html>
<html lang="en-us" ng-app="calcApp" class="full-height">
<head>
    <title>Hello World</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <base href="/Tom-s-Journal/">
    <link rel='shortcut icon' type='image/x-icon' href='favicon.ico' />
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
    <link rel="stylesheet" href="JournalCSS.css">
</head>


<body class="full-height overflow-hidden" ng-cloak>

    <div class="theCalculator" ng-controller="calcCtrl" id="outsideCalcEdge">
        <div class="calcButtonsrow" id="1stLine">
            <div id="answerScreen">
                <span>{{leftOperand}}</span> 
                <span>{{operator}}</span> 
                <span>{{rightOperand}}</span> 
                <span>{{clear}}</span>
            </div>
        </div>

        <div class="calcButtonsrow" title="brand" id="brand">
            <span id="calcBrand">*Basic Calculator*</span><br>
            <span id="kogoSoftwareLLC">Kogo Software LLC</span>
        </div>
        <div class="calcButtonsrow" id="2ndLine">
            <button class="number" id="7" ng-click="setOperand('7')">7</button>
            <button class="number" id="8" ng-click="setOperand('8')">8</button>
            <button class="number" id="9" ng-click="setOperand('9')">9</button>
            <button id="divideBySign" ng-click="setOperator('/')" class=" operator">/</button>
        </div>
        <div class="calcButtonsrow" id="3rdLine">
            <button class="number" id="4" ng-click="setOperand('4')">4</button>
            <button class="number" id="5" ng-click="setOperand('5')">5</button>
            <button class="number" id="6" ng-click="setOperand('6')">6</button>
            <button id="multiplySign" ng-click="setOperator('*')" class=" operator">*</button>
        </div>
        <div class="calcButtonsrow" id="4thLine">
            <button class="number" id="1" ng-click="setOperand('1')">1</button>
            <button class="number" id="2" ng-click="setOperand('2')">2</button>
            <button class="number" id="3" ng-click="setOperand('3')">3</button>
            <button id="minusSign" ng-click="setOperator('-')" class=" operator">-</button>

        </div>
        <div class="calcButtonsrow" id="5thLine">
            <button id="clear" ng-click="setClear('C')">C</button>
            <button class="number" id="0" ng-click="setOperand('0')">0</button>
            <button id="equalSign" ng-click="getAnswer('=')" 
            ng-disabled="!leftOperand || !operator || !rightOperand">=</button>
            <button id="plusSign" ng-click="setOperator('+')" class=" operator">+</button>
        </div>
    </div>


    <script 
    src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.4/angular.min.js">
</script>

<script src="calculator.js"></script>

</body>
</html>

AngularJS

var calcApp = angular.module('calcApp', []);

calcApp.controller('calcCtrl', function ($scope) {
    $scope.leftOperand = "";
    $scope.operator = "";
    $scope.rightOperand = "";
    $scope.answer = "";


    $scope.setOperand = function (operandEntered) {
        if ($scope.operator){
            $scope.rightOperand += operandEntered;
        }
        else {
            $scope.leftOperand += operandEntered;
        };
    };
    $scope.setOperator = function (operatorEntered) {
        $scope.operator = operatorEntered;
    };

    $scope.getAnswer = function () {
        var result = $scope.leftOperand + $scope.operator + $scope.rightOperand;
        var answer = eval(result);
        if (answer % 2 !== 0){
            $scope.answer = answer.toFixed(2);
        }
        else {$scope.answer = answer;}

        $scope.leftOperand = $scope.answer;
        $scope.operator = "";
        $scope.rightOperand = "";
    };

    $scope.setClear = function (a) {
        $scope.clear = location.reload();
    };


});
    var windowProperties = "width=255,height=367,menubar=yes,location=no,resizable=no,scrollbars=no";
    var windowObjectReference = function openCalc() {
        window.open("/Tom-s-Journal/calculatorHTML.html", "calcWindow", windowProperties);
};

CSS

@font-face {
    font-family: 'sickCalculatorFont';
    src: url('./calcFontFiles/digital-7.ttf');
}
#answerScreen {
    background-color: lightgray;
    border-style: inset;
    border-color: white;
    border-width:5px;
    font-family: 'sickCalculatorFont';
    font-size: 25px;
    font-weight: bold;
    height: 50px;
    padding-left: 3px;
    width: 215px;
}

button {
    border-radius: 10px;
    font-weight: bold;
    height: 50px;
    width: 50px;
}



#brand {
    color: #000;
    font-weight: bold;
    font-size: 11px;
    text-align: center;
}

.calcButtonsrow {
    padding: 5px;
}

#calcButtonsBox {
    border-style: groove;
}

#clear {
    background-color: #FFAAAA;
}

.overflow-hidden {
    overflow: hidden;
}
#divideBySign {
    border-radius: 10px;
    font-weight: bold;
    height: 50px;
    width: 50px;
}
#divideBySign:hover {
    background-color: #4CAF50;
    color: white;
}

#equalSign{
    color: #FA6800;
}

#kogoSoftwareLLC {
    color: grey;
    font-weight: bold;
}

#minusSign {
    border-radius: 10px;
    font-weight: bold;
    height: 50px;
    width: 50px;
}
#minusSign:hover {
    background-color: #4CAF50;
    color: white;
}

#modalCalcButt {
    height: 150px !important;
    width: 150px !important;
}

#multiplySign {
    border-radius: 10px;
    font-weight: bold;
    height: 50px;
    width: 50px;
}
#multiplySign:hover {
    background-color: #4CAF50;
    color: white;
}


.number:hover {
    background-color: yellow;
    color: black;
}

#outsideCalcEdge {
    border-style: solid;
    border-width: 3px;
    border-radius: 3px;
    margin-left: 2px;
    margin-bottom: 50px;
    padding: 10px;
    width: 250px;
}

.operator {
    color: white;
    background-color: #0E2F5C;
}

#plusSign {
    border-radius: 10px;
    font-weight: bold;
    height: 50px;
    width: 50px;
}
#plusSign:hover {
    background-color: #4CAF50;
    color: white;
}

.theCalculator {
    background-color: lightskyblue;
}

[disabled] {
    color: gray !important;
    /*            the id of #equalSign takes precedence over [disabled], !important over rides that*/
}

[ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak {
    display: none !important;
}
T Evon
  • 43
  • 8
  • Thanks for the clarification with your JS and HTML. The JS just needs this added to the last line "});" to avoid the "Unexpected End of Input" error. – slevy1 Jun 08 '17 at 20:09
  • i edited the above post, so its working, error free, and ready to go. – T Evon Jun 08 '17 at 22:04
0

short answer - all of the code is further above in a previous answer i made. edited, good to go.

$scope.setOperand = function (operandEntered) {
        if ($scope.operator){
            $scope.rightOperand += operandEntered;
        }
        else {
            $scope.leftOperand += operandEntered;
        };
    };
T Evon
  • 43
  • 8