Meeting planner

Plan one meeting across several time zones. See the local date and time for each participant, check the working hours you enter, and copy a schedule everyone can read.

Loading the meeting planner…

Choose the meeting date first

Enter the proposed date and time in your starting zone, then add the other participants' zones. The meeting planner compares the same event everywhere, including each place's local date and the offset that applies on that date.

If the starting clock skips a time during a forward change, choose another time. When a time repeats during a backward change, select its earlier or later occurrence. The two choices are different instants even though the clock reads the same.

Find overlap using the hours you enter

Preferred hours are optional for each place. Enter the daily window that suits that participant and choose the meeting duration. Common starts must fit the entire meeting inside every enabled window. A window such as 22:00 until 06:00 crosses midnight.

These are your supplied time preferences. The planner has no access to calendars, appointments or personal availability, and it does not assume that everyone works the same hours or takes the same days off. When no full meeting fits, change the date, duration or entered hours.

Send a complete schedule

Copy the comparison after choosing a time. The schedule keeps each place's date, starting time, ending time and UTC offset together, so a next-day meeting is clear to everyone. Up to ten places can be compared.

For current times use the world clock. For a simple pair of zones use the time zone converter.

Build a time zone meeting planner

Resolve the selected meeting date once in the source zone, then convert that exact instant for every participant. The browser helpers below handle repeated/skipped times and compare the complete duration with hours the visitor supplies.

  1. Collect named participant zones, the meeting date, starting time and duration. Keep the source zone explicit.
  2. Reject a skipped wall time. If it repeats, ask for the earlier or later occurrence, then send that chosen absolute instant as at. The normal-date examples below use disambiguation=reject.
  3. Request all destination zones together. Check that every returned clock uses the same Unix instant and that no requested zone is missing.
  4. Use only preferred hours the visitor enables. The browser helper checks every real minute of the full duration, including midnight and clock changes. Common starts belong to the selected source calendar date, which can last 23 or 25 hours.
  5. Show and copy each participant's full local date, start, end and UTC offset. Supplied hours do not read calendars, weekends or personal availability.

Installation steps are included in each example. Set PARSEAPI_KEY on your server. Swift and Kotlin use an app key restricted to your app ID.

// npm install @parseapi/sdk@1.14.0
import { parseAPI } from '@parseapi/sdk';

// Set PARSEAPI_KEY on your server.
const parse = parseAPI();

const destinations = ['Europe/London', 'Asia/Tokyo'];
// This date has one occurrence. For a repeated time use the chosen absolute ISO instant.
const row = await parse.time('America/Denver', { targets: destinations, at: '2026-11-16T09:00:00', disambiguation: 'reject' });
const clocks = [row, ...(row.targets ?? [])].map(clock => ({
  timezone: clock.timezone, at: clock.at, offset: clock.offset, unix: clock.unix,
}));
if (!Number.isSafeInteger(row.unix) || clocks.length !== destinations.length + 1 ||
    clocks.some(clock => !clock.timezone || !clock.at || !clock.offset || clock.unix !== row.unix)) {
  throw new Error('A complete aligned clock comparison is unavailable.');
}
const durationMinutes = 60;
console.log({ instant: row.unix, durationMinutes, clocks });

Choose the intended local time

meeting-time.js
// Resolve a visitor's written date/time before enabling Compare. Do not silently
// move a spring-forward gap. Offer both instants when the local time repeats.
// After that deliberate choice, pass new Date(chosenUnix * 1000).toISOString()
// as SDK at. The server remains the final conversion; compare its returned local
// wall time with this chosen value before applying browser-based preferred hours.
function resolveMeetingTime(date, time, timezone) {
  if (!/^\d{4}-\d{2}-\d{2}$/.test(date) || date.startsWith('0000')
      || !/^(?:[01]\d|2[0-3]):[0-5]\d$/.test(time)) return { kind: 'invalid', instantsUnix: [] };
  const midnight = Date.parse(date + 'T00:00:00Z');
  if (!Number.isFinite(midnight) || new Date(midnight).toISOString().slice(0, 10) !== date) return { kind: 'invalid', instantsUnix: [] };
  let formatter;
  try { formatter = new Intl.DateTimeFormat('en-CA', { timeZone: timezone,
    year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hourCycle: 'h23' }); }
  catch { return { kind: 'invalid', instantsUnix: [] }; }
  const written = Date.parse(date + 'T' + time + ':00Z');
  function local(instant) {
    const p = Object.fromEntries(formatter.formatToParts(instant).map(part => [part.type, part.value]));
    return `${p.year.padStart(4, '0')}-${p.month}-${p.day}T${p.hour}:${p.minute}:${p.second}`;
  }
  const offsets = new Set();
  for (let hours = -48; hours <= 48; hours += 6) {
    const sample = written + hours * 3600000;
    offsets.add(Date.parse(local(sample) + 'Z') - sample);
  }
  const instantsUnix = [...offsets].map(offset => written - offset)
    .filter(instant => local(instant) === date + 'T' + time + ':00')
    .sort((a, b) => a - b).map(instant => instant / 1000);
  return { kind: !instantsUnix.length ? 'gap' : instantsUnix.length === 1 ? 'unique' : 'repeated', instantsUnix };
}

Find common preferred hours

meeting-hours.js
// The SDK returns the same instant for every zone. Apply only hours the visitor
// supplied. These recurring daily clock windows do not imply calendar availability.
// Each start is minute-precise. An end earlier than its start continues overnight.
function meetingMinuteReader(timezone) {
  const formatter = new Intl.DateTimeFormat('en-CA', { timeZone: timezone,
    hour: '2-digit', minute: '2-digit', hourCycle: 'h23' });
  return instant => {
    const p = Object.fromEntries(formatter.formatToParts(instant).map(part => [part.type, part.value]));
    return Number(p.hour) * 60 + Number(p.minute);
  };
}
function meetingWindows(preferences, durationMinutes) {
  if (!Number.isInteger(durationMinutes) || durationMinutes < 1 || durationMinutes > 480) throw new RangeError('Use a duration from 1 to 480 minutes.');
  const time = /^(?:[01]\d|2[0-3]):[0-5]\d$/;
  const minutes = value => { const [h, m] = value.split(':').map(Number); return h * 60 + m; };
  return preferences.filter(row => row.enabled).map(row => {
    if (!time.test(row.start) || !time.test(row.end) || row.start === row.end) throw new RangeError('Enter different start and end hours.');
    return { read: meetingMinuteReader(row.timezone), start: minutes(row.start), end: minutes(row.end) };
  });
}
function meetingMinuteFits(instant, windows) {
  return windows.every(row => { const minute = row.read(instant);
    return row.start < row.end ? minute >= row.start && minute < row.end : minute >= row.start || minute < row.end;
  });
}
function meetingFitsHours(startUnix, durationMinutes, preferences) {
  if (!Number.isSafeInteger(startUnix)) throw new RangeError('Use whole Unix seconds.');
  const windows = meetingWindows(preferences, durationMinutes);
  if (!windows.length) return null; // Hours unknown, not unrestricted availability.
  // Check the entire real interval, including a backward clock change in its middle.
  for (let minute = 0; minute < durationMinutes; minute++) {
    if (!meetingMinuteFits(startUnix * 1000 + minute * 60000, windows)) return false;
  }
  return meetingMinuteFits(startUnix * 1000 + durationMinutes * 60000 - 1, windows);
}
function meetingSourceDay(date, timezone) {
  if (!/^\d{4}-\d{2}-\d{2}$/.test(date) || date.startsWith('0000')) throw new RangeError('Enter a calendar date.');
  const base = Date.parse(date + 'T00:00:00Z');
  if (!Number.isFinite(base) || new Date(base).toISOString().slice(0, 10) !== date) throw new RangeError('Enter a calendar date.');
  const formatter = new Intl.DateTimeFormat('en-CA', { timeZone: timezone, year: 'numeric', month: '2-digit', day: '2-digit' });
  const matches = instant => {
    const p = Object.fromEntries(formatter.formatToParts(instant).map(part => [part.type, part.value]));
    return `${p.year.padStart(4, '0')}-${p.month}-${p.day}` === date;
  };
  let first = null, last = null;
  for (let hour = -48; hour <= 48; hour++) {
    const sample = base + hour * 3600000;
    if (matches(sample)) { first ??= sample; last = sample; }
  }
  if (first === null) throw new RangeError('This date does not exist in the selected source zone.');
  // Local dates can last 23 or 25 hours, or begin after a midnight clock jump.
  let low = first - 3600000, high = first;
  while (high - low > 1) { const middle = Math.floor((high + low) / 2); if (matches(middle)) high = middle; else low = middle; }
  const start = high;
  low = last; high = last + 3600000;
  while (high - low > 1) { const middle = Math.floor((high + low) / 2); if (matches(middle)) low = middle; else high = middle; }
  return { start, end: high };
}
function findCommonMeetingStarts(date, source, durationMinutes, preferences) {
  const windows = meetingWindows(preferences, durationMinutes);
  if (!windows.length) return null;
  const day = meetingSourceDay(date, source);
  const count = Math.ceil((day.end - day.start) / 60000), unavailable = [0];
  // One scan, then a prefix count tests every full duration without reformatting
  // every minute separately for every possible candidate meeting.
  for (let minute = 0; minute < count + durationMinutes; minute++) {
    unavailable.push(unavailable[minute] + (meetingMinuteFits(day.start + minute * 60000, windows) ? 0 : 1));
  }
  const ranges = [];
  for (let minute = 0; minute < count; minute++) {
    if (unavailable[minute + durationMinutes] !== unavailable[minute]) continue;
    const unix = (day.start + minute * 60000) / 1000, previous = ranges.at(-1);
    if (previous && previous.lastUnix + 60 === unix) previous.lastUnix = unix;
    else ranges.push({ firstUnix: unix, lastUnix: unix });
  }
  return ranges; // null = no supplied hours; [] = no full-duration common start.
}

Time API documentationWorld clock

How do I find the best meeting time across time zones?

Enter the meeting date, starting time and source zone, then add the other places. Turn on preferred hours for the participants whose hours you know. The planner checks whether the entire meeting fits those hours and lists common starts on the selected source date.

Does the meeting planner account for daylight saving time?

It uses the rules for the selected meeting date. Places can change clocks on different weekends, so today's difference may not match the meeting date. The result includes local dates and UTC offsets for every participant.

What happens when a meeting time occurs twice or does not exist?

If clocks move backward and the same local time happens twice, choose its earlier or later occurrence. If clocks move forward and skip that time, choose another time. The planner keeps that choice tied to one exact instant for every participant.

Do the suggested times mean everyone is available?

They mean the full duration fits the daily preferred hours you entered. The planner does not read calendars, holidays, weekends or personal commitments. Leave hours off when you do not know them, and confirm the final invitation with the participants.

Can I plan a meeting that crosses midnight?

Yes. A preferred-hours window such as 22:00 to 06:00 continues into the next day. The comparison includes each local start date and an end date when it changes. Common starts belong to the selected source date, even when the meeting ends on the next date.

Why are there no common meeting times?

No start on the selected source date fits the full duration inside all the preferred-hours windows you enabled. Shorten the meeting, try another date, or discuss wider hours with the participants. Hours you leave off do not constrain the comparison.

Can I use this for a recurring meeting?

Check the dates that matter, especially around clock-change weekends. This planner compares one occurrence. Set up the recurring invitation in a calendar using the intended named source zone so future occurrences follow that zone's rules.

What should I send participants?

Choose Copy schedule. It includes the meeting duration and every place's local start date, time, UTC offset and end time. Use those details in the invitation so a participant on a different calendar date sees the correct occurrence.