Writing Unit Tests For Metric-Imperial Converter Project

What’s happening:
I want to write unit tests but I don’t know how to write tests for a specific function given what I have to start with:
1_unit-tests.js:

const chai = require("chai");
let assert = chai.assert;
const ConvertHandler = require("../controllers/convertHandler.js");

let convertHandler = new ConvertHandler();

suite("Unit Tests", () => {
  
});

So what do I have to do here? Thanks in advance for any answers.

And do I need to show my code for api.js and convertHandler.js?

My browser information:

User Agent is: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36 Edg/90.0.818.66

Challenge: Metric-Imperial Converter

Link to the challenge:

Okay, I figured out how to write the unit tests but now I have another problem.

On the specs page, it says:

convertHandler should correctly read a decimal number input.
convertHandler should correctly read a fractional input.
convertHandler should correctly read a fractional input with a decimal.

How are these different? What’s the difference between “decimal input” and “fractional input with a decimal”, even if I assume that “fractional input” would be a number such as 4/5 (4 over 5)?

Thanks.

Edit: Also: is it optional to write the tests, or do we need to write them to pass the challenge?

Turns out I need help in handling the bad input or the situation where only the unit is entered.

Here’s my current code:

function ConvertHandler() {
  const checkUnit = (unit) => {
    if (unit.toLowerCase() !== "gal" || unit.toLowerCase() !== "kg" ||
    unit.toLowerCase() !== "mi" || unit.toLowerCase() !== "km" ||
    unit.toLowerCase() !== "L" || unit.toLowerCase() !== "lbs") {
      throw new Error("invaid unit");
    }
    return true;
  }

  const checkNumberAndUnit = (input) => {
    const [unit] = input.match(/([a-z]+)/i);

    if (Number(input) === NaN && (unit.toLowerCase() !== "gal" || 
    unit.toLowerCase() !== "kg" || unit.toLowerCase() !== "mi" ||
    unit.toLowerCase() !== "km" || unit.toLowerCase() !== "L" ||
    unit.toLowerCase() !== "lbs")) {
      throw new Error("invalid number and unit");
    }
    return true;
  }

  this.getNum = function(input) {
    if (Number(input) === Number.isNaN()) {
      throw new Error("invalid number");
    }

    if (!/^\d+/.test(input)) {
      input = `1${input}`;
    }

    if (checkNumberAndUnit(input)) {
      // extract number part from string containing number and metric unit
      const [number] = input.match(/(\d+(?:.\d+)?)/);
      const result = parseFloat(number);

      return result;
    }
  };

  this.getUnit = function(input) {
    if (checkUnit(input)) {
      const [unit] = input.match(/([a-z]+)/i);
    
      return unit;
    }
  };
  
  this.getReturnUnit = function(initUnit) {
    let result;
    switch (initUnit) {
      case "mi":
        result = "km";
        break;
      case "gal":
        result = "L";
        break;
      case "km":
        result = "mi";
        break;
      case "lbs":
        result = "kg";
        break;
      case "kg":
        result = "lbs";
        break;
      case "L":
        result = "gal";
        break;
      default:
        throw new Error("invalid unit");
    }
    
    return result;
  };

  this.spellOutUnit = function(unit) {
    let result;
    switch (unit) {
      case "mi":
        result = "miles";
        break;
      case "gal":
        result = "gallons";
        break;
      case "km":
        result = "kilometers";
        break;
      case "lbs":
        result = "pounds";
        break;
      case "kg":
        result = "kilograms";
        break;
      case "L":
        result = "liters";
        break;
      default:
        throw new Error("invalid unit");
    }
    
    return result;
  };
  
  this.convert = function(initNum, initUnit) {
    const galToL = 3.78541;
    const lbsToKg = 0.453592;
    const miToKm = 1.60934;
    let result;
    switch (initUnit) {
      case "gal":
        result = initNum * galToL;
        break;
      case "lbs":
        result = initNum * lbsToKg;
        break;
      case "mi":
        result = initNum = miToKm;
        break;
      case "L":
        result = initNum / galToL;
        break;
      case "kg":
        result = initNum / lbsToKg;
        break;
      case "km":
        result = initNum / miToKm;
        break;
    }

    result = result.toPrecision(7);
    
    return result;
  };
  
  this.getString = function(initNum, initUnit, returnNum, returnUnit) {
    let result = `${initNum} ${this.spellOutUnit(initUnit)} converts to ${returnNum} ${this.spellOutUnit(returnUnit)}`;
    
    return result;
  };
  
}

module.exports = ConvertHandler;

Could someone please help me out? Thanks.

Sorry for the multiple posts.

I really can’t get this to work. With what I have right now, it even says “invalid input” when the input string is 3.1mi or 30L. It does reject 3/4/5in but only says “invalid input” rather than “invalid number and input” like it should. I really need some help here.

convetHandler.js:

function ConvertHandler() {
  const checkUnit = (unit) => {
    if (unit.toLowerCase() !== "gal" && unit.toLowerCase() !== "kg" &&
    unit.toLowerCase() !== "mi" && unit.toLowerCase() !== "km" &&
    unit.toLowerCase() !== "L" && unit.toLowerCase() !== "lbs") {
      return false;
    }
    return true;
  }

  const checkNumber = (input) => {
    if (Number(input) === Number.isNaN() || /^\d+(?:(\s(\d)+)*)(?:\/\d+){2,}/.test(input)) {
      return false;
    }
    return true;
  }

  const checkNumberAndUnit = (input) => {
    if (!/^\d+/.test(input)) {
      input = `1${input}`;
    }

    if (!checkNumber(input) && !checkUnit(input)) {
      throw new Error("invalid number and unit");
    } else if (!checkNumber(input)) {
      throw new Error("invalid number");
    } else if (!checkUnit(input)) {
      throw new Error("invalid unit");
    }
    return true;
  }

  this.getNum = function(input) {
    if (/^[a-z]/i.test(input)) {
      // We only got a unit, so the number should be 1
      return 1;
    } else if (checkNumber(input)) {
      // extract number part from string containing number and unit
      const [number] = input.match(/(\d+(?:.\d+)?)/);
      const result = parseFloat(number);

      return result;
    }
  };

  this.getUnit = function(input) {
    if (checkUnit(input)) {
      const [unit] = input.match(/([a-z]+)/i);
    
      return unit;
    }
  };
  
  this.getReturnUnit = function(initUnit) {
    let result;
    switch (initUnit) {
      case "mi":
        result = "km";
        break;
      case "gal":
        result = "L";
        break;
      case "km":
        result = "mi";
        break;
      case "lbs":
        result = "kg";
        break;
      case "kg":
        result = "lbs";
        break;
      case "L":
        result = "gal";
        break;
      default:
        throw new Error("invalid unit");
    }
    
    return result;
  };

  this.spellOutUnit = function(unit) {
    let result;
    switch (unit) {
      case "mi":
        result = "miles";
        break;
      case "gal":
        result = "gallons";
        break;
      case "km":
        result = "kilometers";
        break;
      case "lbs":
        result = "pounds";
        break;
      case "kg":
        result = "kilograms";
        break;
      case "L":
        result = "liters";
        break;
      default:
        throw new Error("invalid unit");
    }
    
    return result;
  };
  
  this.convert = function(initNum, initUnit) {
    if (initNum.toString() === "") {
      initNum = 1;
    }

    const input = `${initNum}${initUnit}`;
    if (checkNumberAndUnit(input)) {
      const galToL = 3.78541;
      const lbsToKg = 0.453592;
      const miToKm = 1.60934;
      let result;
      switch (initUnit) {
        case "gal":
          result = initNum * galToL;
          break;
        case "lbs":
          result = initNum * lbsToKg;
          break;
        case "mi":
          result = initNum = miToKm;
          break;
        case "L":
          result = initNum / galToL;
          break;
        case "kg":
          result = initNum / lbsToKg;
          break;
        case "km":
          result = initNum / miToKm;
          break;
      }

      result = result.toPrecision(7);
    
      return result;
    }
  };
  
  this.getString = function(initNum, initUnit, returnNum, returnUnit) {
    let result = `${initNum} ${this.spellOutUnit(initUnit)} converts to ${returnNum} ${this.spellOutUnit(returnUnit)}`;
    
    return result;
  };
  
}

module.exports = ConvertHandler;

api.js:

"use strict";

const expect = require("chai").expect;
const ConvertHandler = require("../controllers/convertHandler.js");

module.exports = (app) => {
  
  let convertHandler = new ConvertHandler();

  app.route("/api/convert").get((req, res) => {
    try {
      const inputStr = req.query.input;
      const inputNumber = convertHandler.getNum(inputStr);
      const inputUnit = convertHandler.getUnit(inputStr);
      const returnUnit = convertHandler.getReturnUnit(inputUnit);
      const result = convertHandler.convert(inputNumber, inputUnit);
      const fullResultStr = convertHandler.getString(inputNumber, inputUnit, result, returnUnit);
      res.json({
        initNum: inputNumber,
        initUnit: inputUnit,
        returnNum: result,
        returnUnit: returnUnit,
        string: fullResultStr
      });
    } catch (error) {
      console.log("routes/api.js:", error.message);
      res.json({
        error: error.message
      });
    }
  });

};

So what am I doing wrong? Could someone please help me out here? Thanks.

I put the code up on GitHub and made some more changes to try to fix the error handling and get the expected output on each error case. But I still have problems.

Here’s the repository: DragonOsman/dragonosman-metricimpconv-proj (github.com)

Right now when I try 3/4/5mi as the input I get this as the result:

undefined miles converts to 1.609340 kilometers

{"initUnit":"mi","returnNum":"1.609340","returnUnit":"km","string":"undefined miles converts to 1.609340 kilometers"}

Basically, right now it doesn’t say “invalid number and unit” when it should. Same for “invalid number”. It only says “invalid unit” when it should. Someone please help me out here. Thanks.

Edit:
How do I print the error message the way the example app does it? They have it like this:

invalid number and unit

"invalid number and unit"

And why does mine not report “invalid number and unit” like that when both the number and the unit are invalid? What am I missing?

Edit2: Okay, so I just had to do res.json(error.message). Now I just need to know why it’s not reporting “invalid number and unit” when both are invalid, like for 3/4/5in for example.

Here’s my convertHandler.js:

function ConvertHandler() {
  const checkUnit = (unit) => {
    if (unit.toLowerCase() !== "gal" && unit.toLowerCase() !== "kg" &&
    unit.toLowerCase() !== "mi" && unit.toLowerCase() !== "km" &&
    unit.toLowerCase() !== "L" && unit.toLowerCase() !== "lbs") {
      return false;
    }
    return true;
  }

  const checkNumber = (number) => {
    if (Number(number) === Number.isNaN() || /^\d+(?:(\s(\d)+)*)(?:\/\d+){2,}/.test(number)) {
      return false;
    }
    return true;
  }

  const checkNumberAndUnit = (input) => {
    if (!/^\d+/.test(input)) {
      input = `1${input}`;
    }

    const [unit] = input.match(/([a-z]+)/i);
    const [number] = input.match(/^\d+(?:(\.\d))(?:(\s(\d)+)*)(?:\/\d+){1}/);
    if (!checkNumber(number) && !checkUnit(unit)) {
      throw new Error("invalid number and unit");
    } 
    
    if (!checkNumber(input)) {
      throw new Error("invalid number");
    }
    
    if (!checkUnit(unit)) {
      throw new Error("invalid unit");
    }
    return true;
  }

  this.getNum = function(input) {
    if (/^[a-z]/i.test(input)) {
      // We only got a unit, so the number should be 1
      return 1;
    } else if (checkNumber(input)) {
      // extract number part from string containing number and unit
      const [number] = input.match(/(\d+(?:.\d+)?)(?:(\s(\d)+)*)(?:\/\d+){1}/);
      const result = Number(number);

      return result;
    }
  };

  this.getUnit = function(input) {
    const [unit] = input.match(/([a-z]+)/i);
    if (checkUnit(unit)) {
      return unit;
    }
    throw new Error("invalid unit");
  };
  
  this.getReturnUnit = function(initUnit) {
    let result;
    if (initUnit) {
      switch (initUnit.toString().toLowerCase()) {
        case "mi":
          result = "km";
          break;
        case "gal":
          result = "L";
          break;
        case "km":
          result = "mi";
          break;
        case "lbs":
          result = "kg";
          break;
        case "kg":
          result = "lbs";
          break;
        case "L":
          result = "gal";
          break;
        default:
          throw new Error("invalid unit");
      }
      
      return result;
    }
  };

  this.spellOutUnit = function(unit) {
    let result;
    if (unit) {
      switch (unit.toString().toLowerCase()) {
        case "mi":
          result = "miles";
          break;
        case "gal":
          result = "gallons";
          break;
        case "km":
          result = "kilometers";
          break;
        case "lbs":
          result = "pounds";
          break;
        case "kg":
          result = "kilograms";
          break;
        case "L":
          result = "liters";
          break;
        default:
          throw new Error("invalid unit");
      }
      
      return result;
    }
  };
  
  this.convert = function(initNum, initUnit) {
    let input = `${initNum}${initUnit}`;
    if (input.match(/^[a-z]+$/i)) {
      initNum = 1;
      input = `${initNum}${initUnit}`;
    }

    if (checkNumberAndUnit(input)) {
      const galToL = 3.78541;
      const lbsToKg = 0.453592;
      const miToKm = 1.60934;
      let result;
      switch (initUnit.toString().toLowerCase()) {
        case "gal":
          result = initNum * galToL;
          break;
        case "lbs":
          result = initNum * lbsToKg;
          break;
        case "mi":
          result = initNum = miToKm;
          break;
        case "L":
          result = initNum / galToL;
          break;
        case "kg":
          result = initNum / lbsToKg;
          break;
        case "km":
          result = initNum / miToKm;
          break;
      }

      if (result) {
        result = result.toPrecision(7);
    
        return result;
      }
    }
  };
  
  this.getString = function(initNum, initUnit, returnNum, returnUnit) {
    let result = `${initNum} ${this.spellOutUnit(initUnit)} converts to ${returnNum} ${this.spellOutUnit(returnUnit)}`;
    
    return result;
  };
  
}

module.exports = ConvertHandler;

and here’s api.js:

"use strict";

const expect = require("chai").expect;
const ConvertHandler = require("../controllers/convertHandler.js");

module.exports = (app) => {
  
  let convertHandler = new ConvertHandler();

  app.route("/api/convert").get((req, res) => {
    try {
      const inputStr = req.query.input;
      const inputNumber = convertHandler.getNum(inputStr);
      const inputUnit = convertHandler.getUnit(inputStr);
      const returnUnit = convertHandler.getReturnUnit(inputUnit);
      const result = convertHandler.convert(inputNumber, inputUnit);
      const fullResultStr = convertHandler.getString(inputNumber, inputUnit, result, returnUnit);
      res.json({
        initNum: inputNumber,
        initUnit: inputUnit,
        returnNum: result,
        returnUnit: returnUnit,
        string: fullResultStr
      });
    } catch (error) {
      console.log("routes/api.js:", error.message);
      res.json(error.message);
    }
  });

};

So yeah. What am I missing?

Updated code: GitHub - DragonOsman/dragonosman-metricimpconv-proj

I got the error reporting in api.js to how I want it, but the fraction handling is still wacky. When I try 4.5/3lbs as the input, it says:

NaN pounds converts to NaN kilograms

{"initNum":null,"initUnit":"lbs","returnNum":"NaN","returnUnit":"kg","string":"NaN pounds converts to NaN kilograms"}

even though it should give the answer here after evaluating that fraction. I do have code for that in convertHandler.getNum() (although maybe it should be in the convert() method of the same object instead). And when I give it a double fraction like 3/4/5in, it says:

object null is not iterable (cannot read property Symbol(Symbol.iterator))

"object null is not iterable (cannot read property Symbol(Symbol.iterator))"

When it should say:

invalid number and input

"invalid number and input"

So what am I still doing wrong here that’s making it not handle fractions properly?

it prolly has to do with your regex expressions. Check what the particular functions return. Try print(function_that_give_me_error("fraction input that give me error") . Replace my custom names with the respective values. Then in that same function(after you make sure its the one which returns null, or an output that cause following functions to misbehave), put print statements in different parts of its body, to see where exactly the function fails to return the expected result. Double check the regex you use. Test the respective fraction number, against the regex and see if you get the expected value(s)

I checked the regexes on an online regex testing site. The regexes were also working before I added this code:

      // -1 means not found
      if (index !== -1) {
        const numbers = number.split(index);
        const numerator = parseFloat(numbers[0]);
        const denominator = parseFloat(numbers[1]);
        number = numerator / denominator;
      }

Having it inside the getNum function gives “object null is not iterable” error and putting it inside both that one and the convert function gives this error:

NaN pounds converts to undefined kilograms

{"initNum":null,"initUnit":"lbs","returnUnit":"kg","string":"NaN pounds converts to undefined kilograms"}

and having it only inside the convert function give this error:

5.4/3 pounds converts to undefined kilograms

{"initNum":"5.4/3","initUnit":"lbs","returnUnit":"kg","string":"5.4/3 pounds converts to undefined kilograms"}

I had problems with fractions even before adding this code which is why I added it. I think I mentioned those errors on here too.

Use print statements to track where in the problematic getNum function you try to iterate an object which is null. Find what is returning you a null object.

PS: when you explain yourself, be more specific on what you talk about. It can be hard to follow the subject when you say “Having it inside the…”. What is “it”?

I was talking about this code the whole time:

      // -1 means not found
      if (index !== -1) {
        const numbers = number.split(index);
        const numerator = parseFloat(numbers[0]);
        const denominator = parseFloat(numbers[1]);
        number = numerator / denominator;
      }

for convert() it’s like this:

    // to check if the number is a fraction and to evaluate it if so
    const index = initNum.toString().indexOf("/");

    // -1 means not found
    if (index !== -1) {
      const numbers = initNum.split(index);
      const numerator = parseFloat(numbers[0]);
      const denominator = parseFloat(numbers[1]);
      initNum = numerator / denominator;
    }

I was talking about the errors I get when I put this code in getNum() and/or convert().

Here’s getNum:

  this.getNum = function(input) {
    if (/^[a-z]/i.test(input)) {
      // We only got a unit, so the number should be 1
      return 1;
    }

    // extract number part from string containing number and unit
    let [number] = input.match(/(\d+(?:.\d+)?)(?:(\s(\d)+)*)(?:\/\d+){1}/);

    if (checkNumber(number)) {
      // to check if the number is a fraction and to evaluate it if so
      /*const index = number.indexOf("/");
    
      // -1 means not found
      if (index !== -1) {
        const numbers = number.split(index);
        const numerator = parseFloat(numbers[0]);
        const denominator = parseFloat(numbers[1]);
        number = numerator / denominator;
      }*/
      return number;
    } else if (!checkNumber) {
      throw new Error("invalid number");
    } else if (!checkNumberAndUnit(input)) {
      throw new Error("invalid number and unit");
    }
  };

I commented out the fraction checking code to see the error message from only having it inside the convert() function.

convert():

  this.convert = function(initNum, initUnit) {
    if (initNum && initUnit) {
      // to check if the number is a fraction and to evaluate it if so
      const index = initNum.toString().indexOf("/");

      // -1 means not found
      if (index !== -1) {
        const numbers = initNum.split(index);
        const numerator = parseFloat(numbers[0]);
        const denominator = parseFloat(numbers[1]);
        initNum = numerator / denominator;
      }

      const galToL = 3.78541;
      const lbsToKg = 0.453592;
      const miToKm = 1.60934;
      let result;
      switch (initUnit.toString().toLowerCase()) {
        case "gal":
          result = initNum * galToL;
          break;
        case "lbs":
          result = initNum * lbsToKg;
          break;
        case "mi":
          result = initNum = miToKm;
          break;
        case "L":
          result = initNum / galToL;
          break;
        case "kg":
          result = initNum / lbsToKg;
          break;
        case "km":
          result = initNum / miToKm;
          break;
      }

      if (result) {
        result = result.toFixed(5);
    
        return result;
      }
    }
  };

Even with the if condition checking for initNum and initUnit being truthy in convert I still get the error about null not being iterable. The check for fractions in getNum is still commented out.

What’s the best way to extract a number from a string? Number(string)? I need to include fractions, decimals and integers (positive or negative).

Sorry the entire time i say print() when i mean console.log(), as im working on python projects.
Here is part of your convert function:

const numbers = initNum.split(index)

Do i understand correct you try to split the input number by index?

That variable stores the index of the / character. So yes, I’m to split the string using / as the delimiter.

I got this now for convertHandler.js:

function ConvertHandler() {
  const checkUnit = (unit) => {
    if (unit.toLowerCase() !== "gal" && unit.toLowerCase() !== "kg" &&
    unit.toLowerCase() !== "mi" && unit.toLowerCase() !== "km" &&
    unit.toLowerCase() !== "L" && unit.toLowerCase() !== "lbs") {
      return false;
    }
    return true;
  }

  const checkNumber = (number) => {
    if (Number(number.toString()) === Number.isNaN()) {
      return false;
    }
    return true;
  }

  const checkNumberAndUnit = (input) => {
    if (!/^\d+/.test(input)) {
      input = `1${input}`;
    }

    const [unit] = input.match(/([a-z]+)/i);
    const [number] = input.match(/[^a-z]/i);
    if (!checkNumber(number) && !checkUnit(unit)) {
      return false;
    }
    return true;
  }

  this.getNum = function(input) {
    if (/^[a-z]/i.test(input)) {
      // We only got a unit, so the number should be 1
      return 1;
    }

    // extract number part from string containing number and unit
    let numberStr = input.match(/[^a-z]/i);
    numberStr = numberStr.toString();

    // to check if the number is a fraction
    const index = numberStr.indexOf("/");

    // if there's at least one / in there, check if it's the only one
    // -1 means not found
    if (index !== -1) {
      if (numberStr.indexOf("/") === numberStr.lastIndexOf("/")) {
        const numbers = numberStr.split("/");
        const numerator = parseFloat(numbers[0]);
        const denominator = parseFloat(numbers[1]);
        numberStr = (numerator / denominator).toString();
      } else {
        throw new Error("invalid number");
      }
    }

    const number = parseFloat(numberStr);

    if (!checkNumber(number)) {
      throw new Error("invalid number");
    } else if (!checkNumberAndUnit(input)) {
      throw new Error("invalid number and unit");
    }
    return number;
  };

  this.getUnit = function(input) {
    const [unit] = input.match(/([a-z]+)/i);
    if (checkUnit(unit)) {
      return unit;
    }
    throw new Error("invalid unit");
  };
  
  this.getReturnUnit = function(initUnit) {
    let result;
    if (initUnit) {
      switch (initUnit.toString().toLowerCase()) {
        case "mi":
          result = "km";
          break;
        case "gal":
          result = "L";
          break;
        case "km":
          result = "mi";
          break;
        case "lbs":
          result = "kg";
          break;
        case "kg":
          result = "lbs";
          break;
        case "L":
          result = "gal";
          break;
        default:
          throw new Error("invalid unit");
      }
      
      return result;
    }
  };

  this.spellOutUnit = function(unit) {
    let result;
    if (unit) {
      switch (unit.toString().toLowerCase()) {
        case "mi":
          result = "miles";
          break;
        case "gal":
          result = "gallons";
          break;
        case "km":
          result = "kilometers";
          break;
        case "lbs":
          result = "pounds";
          break;
        case "kg":
          result = "kilograms";
          break;
        case "L":
          result = "liters";
          break;
        default:
          throw new Error("invalid unit");
      }
      
      return result;
    }
  };
  
  this.convert = function(initNum, initUnit) {
    if (initNum && initUnit) {
      // to check if the number is a fraction and to evaluate it if so
      const index = initNum.toString().indexOf("/");

      // -1 means not found
      if (index !== -1) {
        const numbers = initNum.split(index);
        const numerator = parseFloat(numbers[0]);
        const denominator = parseFloat(numbers[1]);
        initNum = numerator / denominator;
      }

      const galToL = 3.78541;
      const lbsToKg = 0.453592;
      const miToKm = 1.60934;
      let result;
      switch (initUnit.toString().toLowerCase()) {
        case "gal":
          result = initNum * galToL;
          break;
        case "lbs":
          result = initNum * lbsToKg;
          break;
        case "mi":
          result = initNum = miToKm;
          break;
        case "L":
          result = initNum / galToL;
          break;
        case "kg":
          result = initNum / lbsToKg;
          break;
        case "km":
          result = initNum / miToKm;
          break;
      }

      if (result) {
        result = result.toFixed(5);
    
        return result;
      }
    }
  };
  
  this.getString = function(initNum, initUnit, returnNum, returnUnit) {
    let result = `${initNum} ${this.spellOutUnit(initUnit)} converts to ${returnNum} ${this.spellOutUnit(returnUnit)}`;
    
    return result;
  };
  
}

module.exports = ConvertHandler;

But with this, it truncates floating-point numbers and fractions, and double fractions aren’t detected as invalid numbers.

And I understood that you meant console.log when you said print, so it’s fine.

What did I do wrong here?

you cant split by index number, you must split by the symbol itself. string.split('/').

Yeah, I did that.

But I still have issues.

Latest code for convertHandler.js:

function ConvertHandler() {
  const checkUnit = (unit) => {
    if (unit.toLowerCase() !== "gal" && unit.toLowerCase() !== "kg" &&
    unit.toLowerCase() !== "mi" && unit.toLowerCase() !== "km" &&
    unit.toLowerCase() !== "L" && unit.toLowerCase() !== "lbs") {
      return false;
    }
    return true;
  }

  const checkNumber = (number) => {
    if (Number(number.toString()) === Number.isNaN()) {
      return false;
    }
    return true;
  }

  const checkNumberAndUnit = (input) => {
    if (!/^\d+/.test(input)) {
      input = `1${input}`;
    }

    const [unit] = input.match(/([a-z]+)/i);
    const [number] = input.match(/[^a-z]/i);
    if (!checkNumber(number) && !checkUnit(unit)) {
      return false;
    }
    return true;
  }

  this.getNum = function(input) {
    if (/^[a-z]/i.test(input)) {
      // We only got a unit, so the number should be 1
      return 1;
    }

    // extract number part from string containing number and unit
    let numberStr = input.match(/[^a-z]/i);
    numberStr = numberStr.toString();

    // to check if the number is a fraction
    const index = numberStr.indexOf("/");

    // if there's at least one / in there, check if it's the only one
    // -1 means not found
    if (index !== -1) {
      if (numberStr.indexOf("/") === numberStr.lastIndexOf("/")) {
        const numbers = numberStr.split("/");
        const numerator = parseFloat(numbers[0]);
        const denominator = parseFloat(numbers[1]);
        numberStr = (numerator / denominator).toString();
      } else {
        throw new Error("invalid number");
      }
    }

    const number = parseFloat(numberStr);

    if (!checkNumber(number)) {
      throw new Error("invalid number");
    } else if (!checkNumberAndUnit(input)) {
      throw new Error("invalid number and unit");
    }
    return number;
  };

  this.getUnit = function(input) {
    const [unit] = input.match(/([a-z]+)/i);
    if (checkUnit(unit)) {
      return unit;
    }
    throw new Error("invalid unit");
  };
  
  this.getReturnUnit = function(initUnit) {
    let result;
    if (initUnit) {
      switch (initUnit.toString().toLowerCase()) {
        case "mi":
          result = "km";
          break;
        case "gal":
          result = "L";
          break;
        case "km":
          result = "mi";
          break;
        case "lbs":
          result = "kg";
          break;
        case "kg":
          result = "lbs";
          break;
        case "L":
          result = "gal";
          break;
        default:
          throw new Error("invalid unit");
      }
      
      return result;
    }
  };

  this.spellOutUnit = function(unit) {
    let result;
    if (unit) {
      switch (unit.toString().toLowerCase()) {
        case "mi":
          result = "miles";
          break;
        case "gal":
          result = "gallons";
          break;
        case "km":
          result = "kilometers";
          break;
        case "lbs":
          result = "pounds";
          break;
        case "kg":
          result = "kilograms";
          break;
        case "L":
          result = "liters";
          break;
        default:
          throw new Error("invalid unit");
      }
      
      return result;
    }
  };
  
  this.convert = function(initNum, initUnit) {
    if (initNum && initUnit) {
      const galToL = 3.78541;
      const lbsToKg = 0.453592;
      const miToKm = 1.60934;
      let result;
      switch (initUnit.toString().toLowerCase()) {
        case "gal":
          result = initNum * galToL;
          break;
        case "lbs":
          result = initNum * lbsToKg;
          break;
        case "mi":
          result = initNum = miToKm;
          break;
        case "L":
          result = initNum / galToL;
          break;
        case "kg":
          result = initNum / lbsToKg;
          break;
        case "km":
          result = initNum / miToKm;
          break;
      }

      if (result) {
        result = result.toFixed(5);
    
        return result;
      }
    }
  };
  
  this.getString = function(initNum, initUnit, returnNum, returnUnit) {
    let result = `${initNum} ${this.spellOutUnit(initUnit)} converts to ${returnNum} ${this.spellOutUnit(returnUnit)}`;
    
    return result;
  };
  
}

module.exports = ConvertHandler;

It still truncates fractions and floating-point numbers and doesn’t flag something like 3/4/5 as an invalid number.

And gal flagged as an invalid unit. I don’t get it. I’m pretty sure I wrote the code to have that be one of the valid units. What went wrong?

I hope someone can help me out here.

Updated code on GitHub. I tried making a change in getNum but it didn’t help. I still have the same problems here.

on a short inspection, twice in your code i see you convert the string representation of a unit to lower case and than compare if it is equal to an upper case char, examples:

unit.toLowerCase() !== "L"  //always true

initUnit.toString().toLowerCase()...
  case "L":
    result = "gal"  //never the case

Like i said, place console.log() statements in your code(where you suspect code is breaking), pass problematic parameters to your functions and see what works against your expectations. You could easily pass the problematic unit to one of those functions and see what it returns and where it doesnt return what you suppose it to

PS: the unit should always be a string, no need to convert toString()

For some reason, it seems it’s an object in those functions.

I changed “L” to “l” in the places where I used toLowerCase.

And I need help in figuring out why it truncates floating-point numbers and also only takes the numerator when there’s a fraction. Would math.js help here?

You are overcomplicating here, you don’t need a library. One of the issues is that your regexes are really general: you’re just looking for a-z or 0-9 when you already know what the valid units are and what a valid number looks like.

There are various ways to do this, but as an example:

The input is a string.
Trim the string (remove any whitespace at start and end).
Lowercase the string.

So now you have a sanitised string. You don’t need to lowercase anything in the functions, for example, because the string is always lowercase.

A valid string is made up of a valid number and a valid unit

A valid unit is one of the ones from the list, so put them all in a regex ((lb|kg|etcetc)$. Then if the regex doesn’t match, the input is garbage. You aren’t doing this atm, you’re guessing by checking for characters a-z.

A valid number is either:

  1. a number (which you just use Number to convert).
  2. a number, followed by a /, followed by a number (which you can split on the /, then Number to convert each and divide the first by the second).

A number is:

  • number/s 0-9. Possibly followed by a . and number/s 0-9.

Again, you’re not really checking here, you’re guessing, then checking that guess.

You can write a single regex (with two groups) that runs match on the input.

If that regex matches and you get two values back, you then create that object that’s required by the tests. Any more specific functions can then just use that object to get the output required.

If it doesn’t match, input is garbage and you can do whatever the task requires for bad input.

1 Like

For the number, I tried this:

  this.getNum = function(input) {
    if (/^[a-z]/i.test(input)) {
      // We only got a unit, so the number should be 1
      return 1;
    }

    // extract number part from string containing number and unit
    let numberStr = input.match(/[^a-z]/i);
    numberStr = numberStr.toString();

    // to check if the number is a fraction
    const index = numberStr.indexOf("/");

    // if there's at least one / in there, check if it's the only one
    // -1 means not found
    if (index !== -1) {
      if (numberStr.indexOf("/") === numberStr.lastIndexOf("/")) {
        const numbers = numberStr.split("/");
        const numerator = parseFloat(numbers[0]);
        const denominator = parseFloat(numbers[1]);
        numberStr = (numerator / denominator).toString();
      } else if (numberStr.indexOf("/") !== numberStr.lastIndexOf("/")) {
        throw new Error("invalid number");
      }
    } else {
      const number = parseFloat(numberStr);

      if (!checkNumber(number)) {
        throw new Error("invalid number");
      } else if (!checkNumberAndUnit(input)) {
        throw new Error("invalid number and unit");
      }
      return number;
    }
  };

and checks invalid numbers or units here:

  const checkUnit = (unit) => {
    console.log(typeof unit);
    if (unit.toLowerCase() !== "gal" && unit.toLowerCase() !== "kg" &&
    unit.toLowerCase() !== "mi" && unit.toLowerCase() !== "km" &&
    unit.toLowerCase() !== "l" && unit.toLowerCase() !== "lbs") {
      return false;
    }
    return true;
  }

  const checkNumber = (number) => {
    if (!Number(number.toString())) {
      return false;
    }
    return true;
  }

Aside from the toLowerCase problem, what other issues are there in the unit and number checks? Also including getNum.

Updated code for convertHandler.js file:

function ConvertHandler() {
  const checkUnit = (unit) => {
    console.log(typeof unit);
    if (unit.toLowerCase() !== "gal" && unit.toLowerCase() !== "kg" &&
    unit.toLowerCase() !== "mi" && unit.toLowerCase() !== "km" &&
    unit.toLowerCase() !== "l" && unit.toLowerCase() !== "lbs") {
      return false;
    }
    return true;
  }

  const checkNumber = (number) => {
    if (!Number(number.toString())) {
      return false;
    }
    return true;
  }

  const checkNumberAndUnit = (input) => {
    if (!/^\d+/.test(input)) {
      input = `1${input}`;
    }

    const [unit] = input.match(/(km|kg|L|gal|lbs|mi)$/i);
    const [number] = input.match(/[^a-z]/i);
    if (!checkNumber(number) && !checkUnit(unit)) {
      return false;
    }
    return true;
  }

  this.getNum = function(input) {
    if (/^(km|kg|L|gal|lbs|mi)$/i.test(input)) {
      // We only got a unit, so the number should be 1
      return 1;
    }

    // extract number part from string containing number and unit
    let numberStr = input.match(/[^a-z]/i);
    numberStr = numberStr.toString();

    // to check if the number is a fraction
    const index = numberStr.indexOf("/");

    // if there's at least one / in there, check if it's the only one
    // -1 means not found
    if (index !== -1) {
      if (numberStr.indexOf("/") === numberStr.lastIndexOf("/")) {
        const numbers = numberStr.split("/");
        const numerator = parseFloat(numbers[0]);
        const denominator = parseFloat(numbers[1]);
        numberStr = (numerator / denominator).toString();
      } else if (numberStr.indexOf("/") !== numberStr.lastIndexOf("/")) {
        throw new Error("invalid number");
      }
    } else {
      const number = parseFloat(numberStr);

      if (!checkNumber(number)) {
        throw new Error("invalid number");
      } else if (!checkNumberAndUnit(input)) {
        throw new Error("invalid number and unit");
      }
      return number;
    }
  };

  this.getUnit = function(input) {
    const [unit] = input.match(/(km|kg|L|gal|lbs|mi)$/i);
    return unit.toLowerCase();
  };
  
  this.getReturnUnit = function(initUnit) {
    let result;
    switch (initUnit.toString()) {
      case "mi":
        result = "km";
        break;
      case "gal":
        result = "l";
        break;
      case "km":
        result = "mi";
        break;
      case "lbs":
        result = "kg";
        break;
      case "kg":
        result = "lbs";
        break;
      case "l":
        result = "gal";
        break;
      default:
        throw new Error("invalid unit");
    }
    
    return result;
  };

  this.spellOutUnit = function(unit) {
    let result;
    if (unit) {
      switch (unit.toString()) {
        case "mi":
          result = "miles";
          break;
        case "gal":
          result = "gallons";
          break;
        case "km":
          result = "kilometers";
          break;
        case "lbs":
          result = "pounds";
          break;
        case "kg":
          result = "kilograms";
          break;
        case "l":
          result = "liters";
          break;
        default:
          throw new Error("invalid unit");
      }
      
      return result;
    }
  };
  
  this.convert = function(initNum, initUnit) {
    const galToL = 3.78541;
      const lbsToKg = 0.453592;
      const miToKm = 1.60934;
      let result;
      switch (initUnit.toString()) {
        case "gal":
          result = initNum * galToL;
          break;
        case "lbs":
          result = initNum * lbsToKg;
          break;
        case "mi":
          result = initNum = miToKm;
          break;
        case "l":
          result = initNum / galToL;
          break;
        case "kg":
          result = initNum / lbsToKg;
          break;
        case "km":
          result = initNum / miToKm;
          break;
      }

      if (result) {
        result = result.toFixed(5);
    
        return result;
      }
  };
  
  this.getString = function(initNum, initUnit, returnNum, returnUnit) {
    let result = `${initNum} ${this.spellOutUnit(initUnit)} converts to ${returnNum} ${this.spellOutUnit(returnUnit)}`;
    
    return result;
  };
  
}

module.exports = ConvertHandler;

I did what I could to fix the unit regexes. Need some help on the number validation. Thanks.

You’re doing so much work though. You’ve written very complicated code instead of a single regex. That regex is relatively complicated, but can be built incrementally using a tool like regexr if you’re finding it difficult. So given

regexExample = /^(?<num>validNumberPattern)(?<unit>validUnitPattern)$/;

If I do

matchExample = "12kg".match(example)

Then matchExample.groups should be

{ num: "12", unit: "kg" }

So for example units: why are you not using the regex to check them, why [a-z]? You know what the units have to be to be valid, so why are you not running the regex against those known patterns (eg kg|lbs|km|etc)

So more specifically, stuff like the logic for the fractions: you know what a valid number looks like, so you can check it with regex for validity. You don’t need to take off any letters, see if the string includes a “/”, then loop through the string and so on.

A valid number pattern is what I described in last post.


Once you have a string you think is the number part, just split the string on “/”. If it produces an array with one element, it’s probably not a fraction (just run Number on it). If it produces an array with two elements, it’s probably fraction (run Number on both). If it produces an array with more, it’s garbage.