Writing Unit Tests For Metric-Imperial Converter Project

I took out the regex with a-z for units and have you what you suggested for it now. Did you read my last post?

I have [^a-z] for the numbers because I wanted to exclude all letters so I have only numbers, decimal points and/or / characters left (if it’s valid input).

This is what I’m trying to get at: you know exactly what valid input has to look like. This is what regex is made for: you know the exact pattern. You can write a single regex which tests for a complete valid input, you don’t need to split this logic in the way you’re doing. Furthermore, using the string match method, you can use that regex to give you the two parts directly.

What would be a good regex to match a string that has one or more digits and that might be a floating-point number, and may or may not be the numerator for a fraction whose denominator might have a decimal point too?

I tried this:
/\d+([\/.]{1})?\d*(\.)?(\d)*/
but it only matches floating-point numbers or integers.

And this:
/\d+(\.)*(?:(\/)){1}\
but this matches only one digit after a decimal point and a slash character.

And this:
/\d+(?:([\/\.]))\d*/
matches just the numerator in a fraction, possibly a floating-point number.

If I get the regex and use it in getNum, won’t I still need to evaluate any fractions I may have? I think I might need logic to do that in getNum.

Okay, I just got this regex:
/?\d+(?:\.\d+)?(?:\/\d+(?:\.\d+)?)?/
includes - to also match negative numbers.

Two things to make your life easier

They’re units. You can’t have negative units, it’s impossible.

Also fractional numbers are {integer}/{integer}, they can’t be floating point, that’s also impossible (edit: I mean it’s not impossible if you treat it as just being division, but that’s not fractional numbers)

Actually the example project treats something like 5.4/3lbs as valid input. It’s meant to be evaluated to 1.8 pounds and then converted to kilograms.

Ah, that’s weird but I guess fine. Still, so with what you’ve got:

/?\d+(?:\.\d+)?(?:\/\d+(?:\.\d+)?)?/

That’s close, but try this. Where I’ve written (km), add all possible units, eg (km|mi|kg|lbs).

/^(?<num>\d*(\.\d+)?(\/\d+(\.\d+)?)?)(?<unit>(km))$/

That’s

/
^                 # start of string
(?<num>           # start of named capturing group "num"
\d*               # 0 more digits
(\.\d+)?          # possibly (. then 1 or more numbers)
(\/\d+(\.\d+)?)?  # possibly (/ then 1 or more numbers possibly followed by (a . then 1 or more numbers))
)                 # end of named capturing group "num"
(?<unit>          # start of named capturing group "unit"
(km)              # literal unit NOTE this would be like (km|mi|kg|lb)
)                 # end of named capturing group "unit"
$                 # end of string
/

Then use match. Like input.match(thatPattern). If it doesn’t match, you get null. If it does match, the result will have a property called groups, look at it (so like const result = input.match(thatPattern), then look at result.groups).

Thanks. I’ll try that.

With the first group, you’re going to get one of (numbers are there as example):

{ num: ""}
{ num: "1"}
{ num: "1.2"}
{ num: "1.2/3"}
{ num: "1.2/3.4"}

You need to check for empty string one first (that’s just 1). But otherwise, just split on "/". If you end up with an array with one item, convert that to a number. If you end up with two items, convert them both to numbers and do the division.

How do I just get the last group?

Also, right now it treats gal as L.

Code:

function ConvertHandler() {
  const checkUnit = (unit) => {
    if (!unit.test(/(km|kg|L|lbs|mi|gal)$/i)) {
      return false;
    }
    return true;
  }

  const checkNumber = (number) => {
    if (!/\d*(\.\d+)?(\/\d+(\.\d+)?)?/.test(number.toString())) {
      return false;
    }
    return true;
  }

  const checkNumberAndUnit = (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 (/^(km|kg|L|gal|lbs|mi)$/i.test(input)) {
      // We only got a unit, so the number should be 1
      return 1;
    } else if (/\/$/.test(input)) {
      throw new Error("invalid number");
    }

    let number = 0;
    let result = input.match(/^(?<num>\d*(\.\d+)?(\/\d+(\.\d+)?)?)(?<unit>(km|kg|L|gal|lbs|mi))$/);
    if (result) {
      number = result.groups["num"];
    }

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

    return Number(number.toString());
  };

  this.getUnit = function(input) {
    const [unit] = input.match(/[a-z]$/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 don’t have the regex for matching the numbers or units exactly outside the check* and get* functions (except checkNumberAndUnit, since that one calls checkNumber and checkUnit and they use the exact regexes) because I have to be able to detect and reject invalid input. If I just use the exact regexes throughout the whole code there won’t be any invalid input.

Anyway, yeah, why is it doing that with L and gal? L is still liters, but gal is treated as L.

That regex I suggested applies to the whole input. It’s not two regexes.

So you get the input → sanitise input → run regex → if valid, you get an object with the number part and the unit part → convert number part to actual number.

The result of that would be what you use for your methods, you don’t need a seperate regex for each method. Process the input first, get it to actual numbers and units. Then the methods just use the processed input, they’ll be dumb and extremely easy to test.

If you run it as two, one in each method, then you need to check that there’s a either a number before the unit or the unit is the entire string – as you say, it will match both l and gal because you aren’t doing that

edit:

// paste this into browser console:
unitPattern = /^(?<num>\d*(\.\d+)?(\/\d+(\.\d+)?)?)(?<unit>(km|kg|l|lbs|mi|gal))$/;

inputExamples = [
  // Valid
  "km",
  "1kg",
  "1.2mi",
  "1/2l",
  "1.2/3lbs",
  "1.2/3.4gal",
  // Invalid
  "1om",
  "1..2km",
  "1/2",
  "1/2/3km",
  "1.2.3.4km"
];

for (const input of inputExamples) {
  const matchResult = input.match(unitPattern);
  if (!matchResult) {
    console.warn(`Invalid input: "${input}". Result is null`);
  } else {
    console.log(`Valid input: "${input}". Result is ${JSON.stringify(matchResult.groups)}`);
  }
}

When I enter 4gal, it says it’s 4 liters and converts it to the equivalent number of gallons. 4L or 4l works as expected.

How do I get to the last element in result.groups["num"] though? In the case where the last element has the complete number. Would result.groups["num"][result.groups["num"].length - 1] work?

@DanCouper Okay, here’s the latest code I’m going to try for convertHandler.js:

function ConvertHandler() {
  const checkUnit = (input) => {
    const result = input.match(/^(?<num>\d*(\.\d+)?(\/\d+(\.\d+)?)?)(?<unit>(km|kg|L|gal|lbs|mi))$/i);
    if (!result.groups["unit"].test((/(km|kg|L|gal|lbs|mi))$/i))) {
      throw new Error("invalid unit");
    }
    return true;
  }

  const checkNumber = (input) => {
    const result = input.match(/^(?<num>\d*(\.\d+)?(\/\d+(\.\d+)?)?)(?<unit>(km|kg|L|gal|lbs|mi))$/i);
    if (result.groups["num"].length > 1) {
      if (!result.groups["num"][result.groups["num"].length - 1].test(/(\d*(\.\d+)?(\/\d+(\.\d+)?)?)/)) {
        throw new Error("invalid number");
      }
    }
    return true;
  }

  const checkNumberAndUnit = (input) => {
    if (!checkNumber(input) && !checkUnit(input)) {
      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;
    }

    let number = 0;
    const result = input.match(/^(?<num>\d*(\.\d+)?(\/\d+(\.\d+)?)?)(?<unit>(km|kg|L|gal|lbs|mi))$/i);
    if (result) {
      if (result.groups["num"].length > 1) {
        number = result.groups["num"][result.groups["num"].length - 1];
      }
    }

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

    return Number(number.toString());
  };

  this.getUnit = function(input) {
    const unit = input.match(/[a-z]/i);
    if (!unit.test(/(km|kg|L|gal|lbs|mi)$/i)) {
      throw new Error("invalid unit");
    }
    const result = input.match(/^(?<num>\d*(\.\d+)?(\/\d+(\.\d+)?)?)(?<unit>(km|kg|L|gal|lbs|mi))$/i);

    return result.groups["unit"];
  };
  
  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;
    }
    
    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;
      }
      
      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;

Any problems you can see here that I should fix but I try running it?

Ah, I’m really sorry, I missed a couple of the tests when I was scan reading it: I thought you could just error on invalid input full stop, without need to check which part failed. Ok so what I was saying about the units in the regex: just do

(?<unit>([a-z]+))$ at the end of the regex.

So what will happen there is you get for example:

  • “1/2L” will be {num: "1/2", unit: "l"}, carry on and parse unit. Unit is fine, carry on.
  • “1/2looter” will be { num: "1/2", unit: "looter" } carry on and check unit. Unit is garbage, error.
  • “L” will be {num: "", unit: "l" }, carry on and parse unit. Unit is fine, carry on.
  • “1…4/3/8L” will be null, number is garbage, error.

Yeah, sounds good. I’ll change the regex and alter the checking code as needed.

I have error on line 13 (it seems (I’ll clarify this after this posting the code)):

function ConvertHandler() {
  const checkUnit = (input) => {
    const result = input.match(/^(?<num>\d*(\.\d+)?(\/\d+(\.\d+)?)?)(?<unit>([a-z]+))$/i);
    if (!result.groups["unit"].test((/^(km|kg|L|gal|lbs|mi))$/i))) {
      throw new Error("invalid unit");
    }
    return true;
  }

  const checkNumber = (input) => {
    const result = input.match(/^(?<num>\d*(\.\d+)?(\/\d+(\.\d+)?)?)(?<unit>([a-z]+))$/i);
    if (result.groups["num"].length > 1) {
      if (!result.groups["num"][result.groups["num"].length - 1].test(/(\d*(\.\d+)?(\/\d+(\.\d+)?)?)/)) {
        throw new Error("invalid number");
      }
    }
    return true;
  }

  const checkNumberAndUnit = (input) => {
    if (!checkNumber(input) && !checkUnit(input)) {
      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;
    }

    let number = 0;
    const result = input.match(/^(?<num>\d*(\.\d+)?(\/\d+(\.\d+)?)?)(?<unit>([a-z]+))$/i);

    // if there was a match, result is truthy
    if (result) {
      // if there's more than just digits in the number, it'll be separated
      // so we need to treat it as an array and get at the last element for
      // the full number
      if (result.groups["num"].length > 1) {
        number = result.groups["num"][result.groups["num"].length - 1];

        // check if we've got a fraction (indexOf returns -1 when the character is not found)
        if (number.toString().indexOf("/") !== -1) {
          const numbers = number.toString().split("/");

          // if there are more than two elements in the numbers array, it's invalid
          // because this means it's a double (or more) fraction
          if (numbers.length > 2) {
            throw new Error("invalid number");
          } else if (numbers.length === 2) {
            const numerator = numbers[0];
            const denominator = numbers[1];
            number = Number((numerator / denominator).toString());
          }
        }
      } else if (result.groups["num"].length === 1) {
        number = result.groups["num"];
      }
    }

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

    return number;
  };

  this.getUnit = function(input) {
    const result = input.match(/^(?<num>\d*(\.\d+)?(\/\d+(\.\d+)?)?)(?<unit>([a-z]+))$/i);
    if (!result.groups["unit"].test(/^(km|kg|L|gal|lbs|mi)$/i)) {
      throw new Error("invalid unit");
    }
    return result.groups["unit"];
  };
  
  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;
    }
    
    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;
      }
      
      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;

The error message says:

result.groups.num[(result.groups.num.length - 1)].test is not a function

Line 13 is where I do that.

Anyway. What’s the correct way to do this? If I need to at all. Thanks.

(Are there any other problems or issues I need to be aware of?)

Edit: I chained toString before test there. But now it says

result.groups.num[(result.groups.num.length - 1)].toString(...).test is not a function

It’s this part:

  const checkNumber = (input) => {
    const result = input.match(/^(?<num>\d*(\.\d+)?(\/\d+(\.\d+)?)?)(?<unit>([a-z]+))$/i);
    if (result.groups["num"].length > 1) {
      if (!result.groups["num"][result.groups["num"].length - 1]
        .toString().test(/(\d*(\.\d+)?(\/\d+(\.\d+)?)?)/)) {
        throw new Error("invalid number");
      }
    }
    return true;
  }

How do I do this correctly?

@DanCouper I needed it to work correctly for all uppercase units too, like “KM”, so I used toUpperCase and checked for uppercase units. So now “KM” and “km” both work. But when I try 3/4/5in, it says, “invalid number” instead of “invalid number and unit”. It should report it as the latter.

Here’s my current code:

function ConvertHandler() {
  const checkUnit = (input) => {
    const result = input.match(/^(?<num>\d*(\.\d+)?(\/\d+(\.\d+)?)?)(?<unit>([a-zA-Z]+))$/);
    if (result) {
      switch (result.groups["unit"].toUpperCase()) {
        case "MI":
        case "GAL":
        case "LBS":
        case "KM":
        case "L":
        case "KG":
          return true;
      }
    }
    return false;
  }

  const checkNumber = (input) => {
    const result = input.match(/^\d*(\.\d+)?(\/\d+(\.\d+)?)?/);
    if (!result) {
      return false;
    }
    return true;
  }

  const checkNumberAndUnit = (input) => {
    if (!(checkNumber(input) && !checkUnit(input))) {
      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;
    }

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

    let number = 0;
    const result = input.match(/^(?<num>\d*(\.\d+)?(\/\d+(\.\d+)?)?)(?<unit>([a-zA-Z]+))$/);

    // if there was a match, result is truthy
    if (result) {
      number = result.groups["num"];

      // check if we've got a fraction (indexOf returns -1 when the character is not found)
      if (number.toString().indexOf("/") !== -1) {
        const numbers = number.toString().split("/");

        // if there are more than two elements in the numbers array, it's invalid
        // because this means it's a double (or more) fraction
        if (numbers.length > 2) {
          throw new Error("invalid number");
        } else if (numbers.length === 2) {
          const numerator = numbers[0];
          const denominator = numbers[1];
          number = Number((numerator / denominator).toString());
        }
      } else {
        number = Number(result.groups["num"]);
      }
    } else {
      throw new Error("invalid number");
    }

    return number;
  };

  this.getUnit = function(input) {
    const result = input.match(/^(?<num>\d*(\.\d+)?(\/\d+(\.\d+)?)?)(?<unit>([a-zA-Z]+))$/i);
    return result.groups["unit"];
  };
  
  this.getReturnUnit = function(initUnit) {
    let result;
    switch (initUnit.toString().toUpperCase()) {
      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().toUpperCase()) {
        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().toUpperCase()) {
        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;

Can you tell what the problem might be?

Thanks in advance.

Edit: I was erroneously doing switch (result.groups["num"].toUpperCase()) { in checkUnit earlier which I fixed, but now it’s rejecting even completely valid input with an invalid number and unit error. What did I do wrong? I suspect I messed something up when I tried to make it case-insensitive.

Edit2: I figured out the issue with everything being rejected: the problem was in checkNumberAndUnit where I was negating the if-condition completely.

  const checkNumberAndUnit = (input) => {
    if (!checkNumber(input) && !checkUnit(input)) {
      return false;
    }
    return true;
  }

I had it like this originally: !(checkNumber(input) && !checkUnit(input)). That was probably the reason.

But I need to return this JSON object:

{"initNum":4,"initUnit":"km","returnNum":2.48549,"returnUnit":"mi","string":"4 kilometers converts to 2.48549 miles"}

For both “KM” and “km” for initUnit, for example. Right now it returns one where the initUnit case is dependent on the case it’s entered as, so e.g. entering “KM” would give me an object that has “KM” as initUnit. It should be “km” in either case.

Right, so what I’ve been trying to get across here is (sorry about the issue with the unit error checking needing to be seperate):

You only need to check things once. You shouldn’t need to uppercase anything except L, and even then you can do that at the very end when you’re returning the object that’s expected.

Think of it like a factory assembly line (the order could be slightly different here, but you should be able to get the idea)

Input is a string
Output is the object that’s expected

Trim the string
Lowercase the string
Run the regex.
If it’s null, the number part is wrong, you can error.
If it’s not, you now have an object like { num: "numPart", unit: "unitPart }. Note that I’ve only called the groups “num” and “unit” as examples, what might be better names?
Check the unit part against valid units: these can all be lowercase in the check.
If that check fails, the unit part is wrong, you can error.
If that check passes, set that on the output object
Now parse the number: if it’s an empty string, it’s 1, otherwise convert it to a number, add that to the output object.
Now check what unit you’re converting to, run the conversion and add the output number + unit to the output object.
Now build the output string.
Done.


It should make no difference whatsoever if the input is KG or kg, that isn’t something you should be getting an error on

I’m not getting an error on that. What’s happening is that if I put it as, e.g. “KM”, the initUnit on the returned object is “KM”. Only L should be in uppercase for initUnit on the returned object.

I managed to fix it, but I can’t get away with not using toLowerCase on the unit everywhere even when I’m already using that on the return value for getUnit.

Updated code:

function ConvertHandler() {
  const checkUnit = (input) => {
    const result = input.match(/^(?<num>\d*(\.\d+)?(\/\d+(\.\d+)?)?)(?<unit>([a-zA-Z]+))$/);
    if (result) {
      switch (result.groups["unit"].toLowerCase()) {
        case "mi":
        case "gal":
        case "lbs":
        case "km":
        case "l":
        case "kg":
          return true;
        default:
          return false;
      }
    }
  }

  const checkNumber = (input) => {
    const result = input.match(/^\d*(\.\d+)?(\/\d+(\.\d+)?)?/);
    if (!result) {
      return false;
    }
    return true;
  }

  const checkNumberAndUnit = (input) => {
    if (!checkNumber(input) && !checkUnit(input)) {
      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;
    }

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

    let number = 0;
    const result = input.match(/^(?<num>\d*(\.\d+)?(\/\d+(\.\d+)?)?)(?<unit>([a-zA-Z]+))$/);

    // if there was a match, result is truthy
    if (result) {
      number = result.groups["num"];

      // check if we've got a fraction (indexOf returns -1 when the character is not found)
      if (number.toString().indexOf("/") !== -1) {
        const numbers = number.toString().split("/");

        // if there are more than two elements in the numbers array, it's invalid
        // because this means it's a double (or more) fraction
        if (numbers.length > 2) {
          throw new Error("invalid number");
        } else if (numbers.length === 2) {
          const numerator = numbers[0];
          const denominator = numbers[1];
          number = Number((numerator / denominator).toString());
        }
      } else {
        number = Number(result.groups["num"]);
      }
    } else {
      throw new Error("invalid number");
    }

    return number;
  };

  this.getUnit = function(input) {
    const result = input.match(/^(?<num>\d*(\.\d+)?(\/\d+(\.\d+)?)?)(?<unit>([a-zA-Z]+))$/i);
    return result.groups["unit"].toLowerCase();
  };
  
  this.getReturnUnit = function(initUnit) {
    let result;
    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) {
    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) {
    if (initUnit === "l") {
      initUnit = "L";
    }
    let result = `${initNum} ${this.spellOutUnit(initUnit)} converts to ${returnNum} ${this.spellOutUnit(returnUnit)}`;
    
    return result;
  };
  
}

module.exports = ConvertHandler;

But now, even with this code

    if (initUnit === "l") {
      initUnit = "L";
    }

in getString, I still get l on the object when l was pressed for initUnit. (Again: there’s no error. Just the case (uppercase or lowercase) for initUnit isn’t what it should be)

Okay, never mind; I get a lowercase l on the object for liters whether I enter L or l.

@DanCouper Okay, the uppercase/lowercase thing is solved now. But with my code now it reports the number or unit as invalid correctly when only one is invalid, but when both are invalid it only reports the number as being invalid.

Here’s the current code:

function ConvertHandler() {
  const checkUnit = (input) => {
    const result = input.match(/^(?<num>\d*(\.\d+)?(\/\d+(\.\d+)?)?)(?<unit>([a-z]+))$/i);
    if (!result) {
      return false;
    }

    if (result.groups["unit"] !== "mi" && result.groups["unit"] !== "km" &&
    result.groups["unit"] !== "kg" && result.groups["unit"] !== "lbs" &&
    result.groups["unit"] !== "L" && result.groups["unit"] !== "gal") {
      return false;
    }
    return true;
  }

  const checkNumber = (input) => {
    const result = input.match(/^\d*(\.\d+)?(\/\d+(\.\d+)?)?/);
    if (!result) {
      return false;
    }
    return true;
  }

  const checkNumberAndUnit = (input) => {
    if (!checkNumber(input) && !checkUnit(input)) {
      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;
    }

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

    let number = 0;
    const result = input.match(/^(?<num>\d*(\.\d+)?(\/\d+(\.\d+)?)?)(?<unit>([a-z]+))$/i);

    // if there was a match, result is truthy
    if (result) {
      number = result.groups["num"];

      // check if we've got a fraction (indexOf returns -1 when the character is not found)
      if (number.toString().indexOf("/") !== -1) {
        const numbers = number.toString().split("/");

        // if there are more than two elements in the numbers array, it's invalid
        // because this means it's a double (or more) fraction
        if (numbers.length > 2) {
          throw new Error("invalid number");
        } else if (numbers.length === 2) {
          const numerator = numbers[0];
          const denominator = numbers[1];
          number = Number((numerator / denominator).toString());
        }
      } else {
        number = Number(result.groups["num"]);
      }
    } else {
      throw new Error("invalid number");
    }

    return number;
  };

  this.getUnit = function(input) {
    const result = input.match(/^(?<num>\d*(\.\d+)?(\/\d+(\.\d+)?)?)(?<unit>([a-z]+))$/i);
    let unit = result.groups["unit"];
    if (unit === "l" || unit === "L") {
      return "L";
    }
    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;

Any help is appreciated. Thanks.

You don’t have to write everything in the functions provided. You can have another function that does the inital work.

The way this task is set up is not great. It’s a class, so it would make more sense to have a single conversion method that sets the inputNum, inputUnit , returnUnit and returnNum as properties, then the get functions just get those. Or you ignore the arguments given to the methods, and have a seperate processing function that you just run for every method that returns {inputNum, inputUnit, returnUnit, returnNum, string}. I’m unsure why it’s even set up as a class here, it could just be a single function. Nothing doing though, need to follow the tests, but you can end up with something like:

function parseInput (inputString) {
  // logic
  return parsedInputString
}

function parseNumber (numString) {
  // logic
  return parsedNumString
}

const conversionMap = {
  "km": "mi",
  "mi": "km",
  ...etc
}

class ConvertHandler {
    getNum(input) {
      const result = parseInput(input); // this will throw if the input is invalid
      return parseNum(result.num);
    };
    
    getUnit(input) {
      const result = parseInput(input); // this will throw if the input is invalid
      return result.unit;
    };
    
    getReturnUnit(initUnit) {
      return conversionMap[initUnit];
    };
  
    spellOutUnit(unit) {
      // logic
      return result;
    };
    
    convert(initNum, initUnit) {
      const galToL = 3.78541;
      const lbsToKg = 0.453592;
      const miToKm = 1.60934;
      // conversion logic
      return result;
    };
    
    getString(initNum, initUnit, returnNum, returnUnit) {
      return `${initNum} {initUnit} converts to {returnNum} {returnUnit}`;
    };
}