Skip to content

Commit

Permalink
options-sheets: VESTING_SIMPLE: add simplified version
Browse files Browse the repository at this point in the history
  • Loading branch information
lyind committed Oct 30, 2024
1 parent 809121d commit d8d7b16
Show file tree
Hide file tree
Showing 2 changed files with 99 additions and 28 deletions.
43 changes: 43 additions & 0 deletions lib/Util.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,4 +148,47 @@ class Util {
return (sorted[mid - 1] + sorted[mid]) / 2.0;
}
}


/** Add calendar months (months are a thing with many definitions) to a date.
*
* Adapted from: https://stackoverflow.com/a/2706169
*
* @param {Date} date The date to add months to. The original date is not modified.
* @param {number} months The number of months to add.
* @return {Date} A new date instance with the specified number of months added.
*/
static addMonths(date, months) {
const result = new Date(date);
result.setMonth(result.getMonth() + +months);
if (result.getDate() !== date.getDate()) {
result.setDate(0);
}
return result;
}


/** Return the number of full months between two dates.
*
* @param {Date} date1 First date.
* @param {Date} date2 Second date.
* @param {boolean} roundUpMonths If true, round up fractional months.
* @return {number|number}
*/
static monthDiff(date1, date2, roundUpMonths = false) {
const inverse = date1 > date2;
const startDate = inverse ? date2 : date1;
const endDate = inverse ? date1 : date2;

let months = ((endDate.getFullYear() - startDate.getFullYear()) * 12)
+ (endDate.getMonth() - startDate.getMonth());

if (roundUpMonths && endDate.getDate() > startDate.getDate()) {
++months;
} else if (!roundUpMonths && endDate.getDate() < startDate.getDate()) {
--months;
}

return inverse ? -1 * months : months;
};
}
84 changes: 56 additions & 28 deletions options-sheets/Options.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
* Managed via: https://github.com/giantswarm/gapps-automation
*/

const DAYS_PER_MONTH = 30.42;

const VESTING_CLIFF_MONTHS = 24;


Expand All @@ -18,28 +16,26 @@ const VESTING_CLIFF_MONTHS = 24;
* 25% vested,
* 50% vested,
* 100% Vested,
* Excluded Months,
* Reference Date (pass string 'NOW' to use the current date)
* @param richOutput Output reasons for vested shares == 0
* @return The amount of vested shares at the specified point in time (one column per input row).
* @return The amount of vested shares at the specified point in time and the total finished months considered (two columns per input row).
* @customfunction
*/
function VESTING(input, richOutput) {
if (!Array.isArray(input) || (input.length > 0 && input[0].length < 7)) {
throw new Error('Invalid input range, expecting rows with at least 7 columns');
if (!Array.isArray(input) || (input.length > 0 && input[0].length < 6)) {
throw new Error('Invalid input range, expecting rows with at least 6 columns');
}

// transform each row into new row with one column (# of shares vested)
return input.map(([shares, start, end1, end2, end3, excludedMonths, nowArg]) => {
return input.map(([shares, start, end1, end2, end3, nowArg]) => {

//const dumpArgs = () => `shares=${shares}, start=${start}, end1=${end1}, end2=${end2}, end3=${end3}, excludedMonths=${excludedMonths}, now=${nowArg}`;
//const dumpArgs = () => `shares=${shares}, start=${start}, end1=${end1}, end2=${end2}, end3=${end3}, now=${nowArg}`;
try {
return calculateVesting(+shares,
new Date(start),
new Date(end1),
new Date(end2),
new Date(end3),
+excludedMonths,
(nowArg === 'NOW' || nowArg === 'now') ? new Date() : new Date(nowArg),
!!richOutput);
} catch (e) {
Expand All @@ -49,16 +45,49 @@ function VESTING(input, richOutput) {
}


/** Calculate shares vested at referenceDate. */
const calculateVesting = function (shares, start, end1, end2, end3, excludedMonths, now, richOutput) {
/**
* Calculates the shares vested at a certain point in time.
*
* This variant assumes that the shares are vested in consecutive batches and vesting periods and omits "excluded months".
*
* Vesting period duration is assumed to be 12 months each.
*
* @param input {Array<Array<any>>} The shares and vesting info, the following fields are required:
* Vesting Start Date,
* Reference Date (pass string 'NOW' to use the current date),
* Amount of shares
* @param richOutput Output reasons for vested shares == 0
* @return The amount of vested shares at the specified point in time and the total finished months considered (two columns per input row).
* @customfunction
*/
function VESTING_SIMPLE(input, richOutput) {
if (!Array.isArray(input) || (input.length > 0 && input[0].length < 3)) {
throw new Error('Invalid input range, expecting rows with at least 3 columns (start date, reference date, shares)');
}

const isMonotonic = values => values.every((value, index, array) => (index) ? value >= array[index - 1] : true);
// transform each row into new row with two columns (# of shares vested, full months considered)
return input.map(([start, nowArg, shares]) => {

const milliesToDays = seconds => seconds / MILLISECONDS_PER_DAY;
//const dumpArgs = () => `start=${start}, nowArg=${nowArg}, shares=${shares}`;
try {
return calculateVesting(+shares,
new Date(start),
Util.addMonths(start, VESTING_CLIFF_MONTHS),
Util.addMonths(start, VESTING_CLIFF_MONTHS + 12),
Util.addMonths(start, VESTING_CLIFF_MONTHS + 12 + 12),
(nowArg === 'NOW' || nowArg === 'now') ? new Date() : new Date(nowArg),
!!richOutput);
} catch (e) {
return richOutput ? '' + e.message : null;
}
});
}

const milliesToMonths = seconds => Math.ceil(milliesToDays(seconds)) / DAYS_PER_MONTH;

const monthsToMillies = months => months * DAYS_PER_MONTH * MILLISECONDS_PER_DAY;
/** Calculate shares vested at referenceDate. */
const calculateVesting = function (shares, start, end1, end2, end3, now, richOutput) {

const isMonotonic = values => values.every((value, index, array) => (index) ? value >= array[index - 1] : true);

// plausibility checks
if (![start, end1, end2, end3, now].every(d => d instanceof Date && !isNaN(d))) {
Expand All @@ -67,22 +96,22 @@ const calculateVesting = function (shares, start, end1, end2, end3, excludedMont
throw new Error('vesting period dates not set or not monotonic');
} else if (typeof shares !== 'number' || shares < 0) {
throw new Error('parameter shares must be a number >= 0');
} else if (typeof excludedMonths !== 'number' || excludedMonths < 0) {
throw new Error('parameter excludedMonths must be a number >= 0');
}

const endOfCliff = Util.addMonths(start, VESTING_CLIFF_MONTHS);
if (now < start) {
return richOutput ? 'vesting not started yet' : 0;
} else if (now < start + monthsToMillies(VESTING_CLIFF_MONTHS)) {
const cliffMonthsRemaining = milliesToMonths((start + monthsToMillies(VESTING_CLIFF_MONTHS)) - now);
return richOutput ? 'vesting cliff not reached, yet: remaining months: ' + Math.ceil(cliffMonthsRemaining) : 0;
return richOutput ? [0, 'vesting not started yet'] : [0, 0];
} else if (now < endOfCliff) {
const cliffMonthsRemaining = Util.monthDiff(endOfCliff, now);
return richOutput ? [0, 'vesting cliff not reached, yet: remaining months: ' + Math.ceil(cliffMonthsRemaining)] : [0, 0];
}

const vesting25_months_total = Math.max(0, milliesToMonths(end1 - start));
const vesting50_months_total = Math.max(0, milliesToMonths(end2 - end1));
const vesting100_months_total = Math.max(0, milliesToMonths(end3 - end2));

let vestedMonths = Math.max(0, milliesToMonths(now - start) - excludedMonths);
const vesting25_months_total = Math.max(0, Util.monthDiff(start, end1));
const vesting50_months_total = Math.max(0, Util.monthDiff(end1, end2));
const vesting100_months_total = Math.max(0, Util.monthDiff(end2, end3));

let totalVestedMonths = Math.max(0, Util.monthDiff(start, now));
let vestedMonths = totalVestedMonths;
let vestedShares = 0.0;

// phase 1 (0 -> 25%)
Expand All @@ -101,7 +130,6 @@ const calculateVesting = function (shares, start, end1, end2, end3, excludedMont
const phase3Months = Math.min(vestedMonths, vesting100_months_total);
const phase3Shares = shares * 0.50;
vestedShares += vesting100_months_total > 0 ? ((phase3Months * phase3Shares) / vesting100_months_total) : phase3Shares;
vestedMonths -= phase3Months;

return +(new Number(vestedShares).toFixed(2));
return [+(new Number(vestedShares).toFixed(2)), +(new Number(totalVestedMonths).toFixed(2))];
};

0 comments on commit d8d7b16

Please sign in to comment.