How does one parse argument values from a function with a varying arguments signature?

124 Views Asked by At

I have a function called "Time", it accepts from 0 to 6 arguments.

function Time(year,month,day,hour,minutes,options) {

}

Parameters month, day, hour and minutes cannot be defined if the previous one isn't defined.

Even if the "options" parameter is optional, if present, it has to be the last parameter.

However, user can define only year and month for example, and then options.

How to avoid the user to have to write this:

var newTimeInstance = Time(2024,06, undefined, undefined, undefined, options {
   fr: {
     // ...
   },
   es: {
     // ...
   }
})

If it's not possible, is there another way to join options to a function ?

I've tried: var newTimeInstance = Time(2024,01).options({ ... }) but then I'm unable to access the other constructor functions in newTimeInstance.

4

There are 4 best solutions below

3
Eric Fortis On

Inconvenient, but you could do:

function Time(...args) {
  if (args.length === 3) {
    const [year, month, options] = args
    console.log({
      year,
      month,
      options
    })
  }
  // other cases
}

Time(2024, 4, {
  extra: 'options'
})

0
Alexander Nenashev On

Use Array::reduceRight() to check arguments:

function Time(year,month,day,hour,minutes,options) {
  [...arguments].reduceRight((r, arg, idx, arr) => {
    if(arg === undefined && arr[idx + 1] !== undefined) throw new Error('An argument cannot be undefined');
  });
}

Time(2024, 3, undefined); // ok
Time(undefined, 2); // an error

0
Peter Seliger On

How about a custom arguments parsing approach, covered by a utility/helper function, where one in addition, at a more detailed scale, would decide whether an arguments signature is valid and whether to sanitize some values or throw an error ...

function isObjectObject(value) {
  return (/^\[object\s+Object\]$/).test(
    Object.prototype.toString.call(value)
  );
}

function parseArgumentsSignature(args) {
  const params = [];
  let options;
  let error;

  args
    .slice(0, 6)

    // every allows an early exit while using a callback function.
    .every((value, idx) => {
      let proceed = true;

      if (isObjectObject(value)) {

        options = value;
        proceed = false;

      } else if (value == null) {

        params[idx] = undefined;
      } else {
        value = parseInt(value, 10);
        if (Number.isFinite(value)) {

          params[idx] = value;
        } else {
          error = new TypeError('Invalid arguments signature.');

          proceed = false;
        }
      }
      return proceed;
    });

  return (error && {
    error,
  } || {
    params, 
    ...(options && { options } || {}),
  });
}

function Time(...args/* year, month, day, hour, minutes, options */) {
  const parsed = parseArgumentsSignature(args);

  if (Object.hasOwn(parsed, 'error')) {
    throw parsed.error;
  }
  const {
    params: [
      year = 0, month = 0, day = 0, hour = 0, minutes = 0,
    ],
    options = null,
  } =
    parsed;

  console.log({
    year, month, day, hour, minutes, options, params: parsed.params,
  });
}

console.log(
  "Time(...[2024, 06,,, null, null, { fr: 'fr', es: 'es' }]);"
);
Time(...[2024, 06,,, null, null, { fr: 'fr', es: 'es' }]);

console.log(
  "Time(...[2024, 06,,,, { fr: 'fr', es: 'es' }]);"
);
Time(...[2024, 06,,,, { fr: 'fr', es: 'es' }]);

console.log(
  "Time(2024, { fr: 'fr', es: 'es' });"
);
Time(2024, { fr: 'fr', es: 'es' });

console.log(
  "Time(2024, 06);"
);
Time(2024, 06);

console.log(
  "Time(2024, 'foo bar');"
);
Time(2024, 'foo bar');
.as-console-wrapper { min-height: 100%!important; top: 0; }

The easiest way of cause is a change of the function's variadic arguments signature towards a single object-based configuration-like parameter.

function Time({
  year = 0, month = 0, day = 0, hour = 0, minutes = 0, options = {},
}) {
  console.log({
    year, month, day, hour, minutes, options
  });
}

console.log(
  "Time({ year: 2024, month: 6, options: { fr: 'fr', es: 'es' } });"
);
Time({ year: 2024, month: 6, options: { fr: 'fr', es: 'es' } });

console.log(
  "Time({ year: 2024, options: { fr: 'fr', es: 'es' } });"
);
Time({ year: 2024, options: { fr: 'fr', es: 'es' } });

console.log(
  "Time({ year: 2024, month: 6 });"
);
Time({ year: 2024, month: 6 });
.as-console-wrapper { min-height: 100%!important; top: 0; }

0
KooiInc On

Create an Object for arguments with a default empty value ({param1, param2, ..., paramx} = {}, aka destructuring assigment).

That way you'll always have named variables. It's a bit more typing calling the function, but it offers better (named) parameter handling, flexibility (e.g. you can add parameters without breaking/necessarily having to refactor existing calls to the function), the user just has to supply the arguments needed and doesn't need to provide them in a fixed order.

Looks like:

function Time({year,month,day,hour,minutes,options = `dst:0`} = {}) {
  console.log(`your input: ${year ? `year: ${year}` : ``}${
    month ? `, month: ${month}` : ``}${
    day ? `, day: ${day}` : ``}${
    hour ? `, hour: ${hour}` : ``}${
    minutes ? `, minutes: ${minutes}` : ``} (options: ${options})`);
}

Time();
Time({year: 2024, hour: 12, options: `dst:1`});
Time(`this is not available`);
Time({year: 2024, month: 0, day: 21, hour: 12, minutes: 30,});

const now = new Date();
const params = {
  year: now.getFullYear(),
  month: now.getMonth(),
  day: now.getDate(),
  hour: now.getHours(),
  minutes: now.getMinutes(),
  options: `dst:1`
};
Time(params);
.as-console-wrapper {
    max-height: 100% !important;
}