Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

options-sheets: VESTING_SIMPLE(): add simplified version #209

Merged
merged 1 commit into from
Oct 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions lib/Util.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,4 +148,49 @@ 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.
*
* Adapted from: https://stackoverflow.com/a/26930998
*
* @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))];
};
Loading