From 030116ba7128a2f7d3fb94526a59febba97bfe1b Mon Sep 17 00:00:00 2001 From: tobiadefami Date: Wed, 7 Oct 2026 22:12:56 +0100 Subject: [PATCH 1/4] Add a day-count helper and use it in YEARFRAC DateTimeHelper.dayCountByBasis(startDate, endDate, basis) returns the day count and the days in the year for day-count bases 0-4. Basis 0 applies toBasisUS to copies of the dates, basis 1 uses yearLengthForBasis, and basis 4 uses toBasisEU. YEARFRAC now returns dayCount / yearDays from the helper. Its results are unchanged. DAYS360 keeps its own days360Core and is not touched. Co-Authored-By: Claude Opus 5.5 --- src/DateTimeHelper.ts | 45 ++++++++++++++++++++++++ src/interpreter/plugin/DateTimePlugin.ts | 18 ++-------- 2 files changed, 47 insertions(+), 16 deletions(-) diff --git a/src/DateTimeHelper.ts b/src/DateTimeHelper.ts index aab02290c5..d2de40838e 100644 --- a/src/DateTimeHelper.ts +++ b/src/DateTimeHelper.ts @@ -211,6 +211,43 @@ export class DateTimeHelper { } } + /** + * Counts the days between two dates under a day-count basis, and returns the number of days in a year under the + * same basis. YEARFRAC and the securities functions (DISC, INTRATE, PRICEDISC, RECEIVED, YIELDDISC, ACCRINTM) + * compute their year fraction as `dayCount / yearDays`. + * + * | `basis` | `dayCount` | `yearDays` | + * |---------|----------------------------------------------|-----------------------------| + * | 0 | US (NASD) 30/360, as adjusted by `toBasisUS` | 360 | + * | 1 | actual days | `yearLengthForBasis` | + * | 2 | actual days | 360 | + * | 3 | actual days | 365 | + * | 4 | European 30/360, as adjusted by `toBasisEU` | 360 | + * + * @param {number} startDate - date serial number, an integer not greater than `endDate` + * @param {number} endDate - date serial number, an integer + * @param {number} basis - day-count basis, an integer from 0 to 4 + */ + public dayCountByBasis(startDate: number, endDate: number, basis: number): { dayCount: number, yearDays: number } { + const start = this.numberToSimpleDate(startDate) + const end = this.numberToSimpleDate(endDate) + switch (basis) { + case 0: { + const [adjustedStart, adjustedEnd] = this.toBasisUS({...start}, {...end}) + return {dayCount: daysIn30DayMonths(adjustedStart, adjustedEnd), yearDays: 360} + } + case 1: + return {dayCount: endDate - startDate, yearDays: this.yearLengthForBasis(start, end)} + case 2: + return {dayCount: endDate - startDate, yearDays: 360} + case 3: + return {dayCount: endDate - startDate, yearDays: 365} + case 4: + return {dayCount: daysIn30DayMonths(toBasisEU(start), toBasisEU(end)), yearDays: 360} + } + throw new Error('Should not be reachable.') + } + private parseSingleFormat(dateString: string, dateFormat?: string, timeFormat?: string): Maybe { const dateTime = this.parseDateTime(dateString, dateFormat, timeFormat) if (instanceOfSimpleDate(dateTime)) { @@ -319,3 +356,11 @@ export function timeToNumber(time: SimpleTime): number { export function toBasisEU(date: SimpleDate): SimpleDate { return {year: date.year, month: date.month, day: Math.min(30, date.day)} } + +/** + * Counts the days between two dates as if every month had 30 days. The dates must already be adjusted to a 30/360 + * basis (`toBasisUS` or `toBasisEU`). + */ +function daysIn30DayMonths(start: SimpleDate, end: SimpleDate): number { + return 360 * (end.year - start.year) + 30 * (end.month - start.month) + end.day - start.day +} diff --git a/src/interpreter/plugin/DateTimePlugin.ts b/src/interpreter/plugin/DateTimePlugin.ts index fd1d3fad01..366c5a0db7 100644 --- a/src/interpreter/plugin/DateTimePlugin.ts +++ b/src/interpreter/plugin/DateTimePlugin.ts @@ -532,22 +532,8 @@ export class DateTimePlugin extends FunctionPlugin implements FunctionPluginType if (startDate > endDate) { [startDate, endDate] = [endDate, startDate] } - switch (mode) { - case 0: - return this.days360Core(startDate, endDate, false) / 360 - case 1: - return (endDate - startDate) / this.dateTimeHelper.yearLengthForBasis( - this.dateTimeHelper.numberToSimpleDate(startDate), - this.dateTimeHelper.numberToSimpleDate(endDate) - ) - case 2: - return (endDate - startDate) / 360 - case 3: - return (endDate - startDate) / 365 - case 4: - return this.days360Core(startDate, endDate, true) / 360 - } - throw new Error('Should not be reachable.') + const {dayCount, yearDays} = this.dateTimeHelper.dayCountByBasis(startDate, endDate, mode) + return dayCount / yearDays } ) } From 5de94b960ef53b9572c9376fdbcf9c30c356fd1a Mon Sep 17 00:00:00 2001 From: tobiadefami Date: Wed, 7 Oct 2026 22:12:57 +0100 Subject: [PATCH 2/4] Add the ACCRINTM, DISC, INTRATE, PRICEDISC, RECEIVED and YIELDDISC functions Six financial functions for discounted and interest-at-maturity securities, built on DateTimeHelper.dayCountByBasis. - Every argument is a SCALAR and is converted in the method, so booleans and the empty text give #VALUE! and invalid arguments are reported in Excel's order (dates, basis, then the amounts for the DISC-like functions; left to right for ACCRINTM). - Basis is truncated, but a negative basis is #NUM!, as in Excel. - Dates are truncated; dates outside the supported range give #NUM!. - The arithmetic follows the operation order that reproduces Excel's results to the 15th significant digit. - An empty or omitted ACCRINTM par is 1000. Excel rejects the 3-argument form; this is added to the list of differences. Adds catalogue entries, names in all language packs and a changelog entry. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 4 + docs/guide/list-of-differences.md | 1 + src/i18n/languages/csCZ.ts | 6 + src/i18n/languages/daDK.ts | 6 + src/i18n/languages/deDE.ts | 6 + src/i18n/languages/enGB.ts | 6 + src/i18n/languages/esES.ts | 6 + src/i18n/languages/fiFI.ts | 6 + src/i18n/languages/frFR.ts | 6 + src/i18n/languages/huHU.ts | 6 + src/i18n/languages/idID.ts | 6 + src/i18n/languages/itIT.ts | 6 + src/i18n/languages/nbNO.ts | 6 + src/i18n/languages/nlNL.ts | 6 + src/i18n/languages/plPL.ts | 6 + src/i18n/languages/ptPT.ts | 6 + src/i18n/languages/ruRU.ts | 6 + src/i18n/languages/svSE.ts | 6 + src/i18n/languages/trTR.ts | 6 + .../functionMetadata/categories/financial.ts | 42 +++ src/interpreter/plugin/FinancialPlugin.ts | 284 ++++++++++++++++++ 21 files changed, 433 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index aeea8dd511..985f015c97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +### Added + +- Added new functions: ACCRINTM, DISC, INTRATE, PRICEDISC, RECEIVED, YIELDDISC. + ### Fixed - Fixed the `AVERAGEIF` function returning a division-by-zero error when the calculated average was `0`. [#1733](https://github.com/handsontable/hyperformula/pull/1733) diff --git a/docs/guide/list-of-differences.md b/docs/guide/list-of-differences.md index a109f82616..05bc580620 100644 --- a/docs/guide/list-of-differences.md +++ b/docs/guide/list-of-differences.md @@ -77,6 +77,7 @@ To remove the differences, create [custom implementations](custom-functions.md) | LCMP | =LCM(1000000, 1000001, 1000002, 1000003) | NUM | 5.00003E+23 | NUM | | TBILLPRICE | =TBILLPRICE(0, 180, 1.9) | 5.0000 | NUM | 5.0000 | | TBILLPRICE | =TBILLPRICE(0, 180, 2) | 0.0000 | NUM | 0.0000 | +| ACCRINTM | =ACCRINTM(DATE(2008,4,1), DATE(2008,6,15), 0.1) | 20.5556 | 20.5556 | Wrong number | | NPV | =NPV(1, TRUE(), 1) | 0.7500 | 0.5000 | 0.7500 | | NPV | =NPV(1,B1) where B1 = true | 0.5000 | 0.0000 | 0.0000 | | POISSON.DIST | =POISSON.DIST(-0.01, 0, FALSE()) | NUM | 1.0000 | NUM | diff --git a/src/i18n/languages/csCZ.ts b/src/i18n/languages/csCZ.ts index fea286083c..5ae7308e86 100644 --- a/src/i18n/languages/csCZ.ts +++ b/src/i18n/languages/csCZ.ts @@ -25,6 +25,7 @@ const dictionary: RawTranslationPackage = { 'ARRAY_CONSTRAIN': 'ARRAY_CONSTRAIN', ARRAYFORMULA: 'ARRAYFORMULA', ABS: 'ABS', + ACCRINTM: 'ACCRINTM', ACOS: 'ARCCOS', ACOSH: 'ARCCOSH', ACOT: 'ACOT', @@ -96,6 +97,7 @@ const dictionary: RawTranslationPackage = { DECIMAL: 'DECIMAL', DEGREES: 'DEGREES', DELTA: 'DELTA', + DISC: 'DISC', DOLLARDE: 'DOLLARDE', DOLLARFR: 'DOLLARFR', EDATE: 'EDATE', @@ -124,6 +126,7 @@ const dictionary: RawTranslationPackage = { INDEX: 'INDEX', INT: 'CELÁ.ČÁST', INTERVAL: 'INTERVAL', //FIXME + INTRATE: 'INTRATE', IPMT: 'PLATBA.ÚROK', IRR: 'MÍRA.VÝNOSNOSTI', ISBINARY: 'ISBINARY', @@ -184,11 +187,13 @@ const dictionary: RawTranslationPackage = { PRODUCT: 'SOUČIN', POWER: 'POWER', PPMT: 'PLATBA.ZÁKLAD', + PRICEDISC: 'PRICEDISC', PROPER: 'VELKÁ2', PV: 'SOUČHODNOTA', RADIANS: 'RADIANS', RAND: 'NÁHČÍSLO', RATE: 'ÚROKOVÁ.MÍRA', + RECEIVED: 'RECEIVED', REPLACE: 'NAHRADIT', REPT: 'OPAKOVAT', RIGHT: 'ZPRAVA', @@ -259,6 +264,7 @@ const dictionary: RawTranslationPackage = { XOR: 'XOR', YEAR: 'ROK', YEARFRAC: 'YEARFRAC', + YIELDDISC: 'YIELDDISC', ROMAN: 'ROMAN', ARABIC: 'ARABIC', 'HF.ADD': 'HF.ADD', diff --git a/src/i18n/languages/daDK.ts b/src/i18n/languages/daDK.ts index 9264551249..38df364820 100644 --- a/src/i18n/languages/daDK.ts +++ b/src/i18n/languages/daDK.ts @@ -25,6 +25,7 @@ const dictionary: RawTranslationPackage = { 'ARRAY_CONSTRAIN': 'ARRAY_CONSTRAIN', ARRAYFORMULA: 'ARRAYFORMULA', ABS: 'ABS', + ACCRINTM: 'PÅLØBRENTE.UDLØB', ACOS: 'ARCCOS', ACOSH: 'ARCCOSH', ACOT: 'ARCCOT', @@ -96,6 +97,7 @@ const dictionary: RawTranslationPackage = { DECIMAL: 'DECIMAL', DEGREES: 'GRADER', DELTA: 'DELTA', + DISC: 'DISKONTO', DOLLARDE: 'KR.DECIMAL', DOLLARFR: 'KR.BRØK', EDATE: 'EDATO', @@ -124,6 +126,7 @@ const dictionary: RawTranslationPackage = { INDEX: 'INDEKS', INT: 'HELTAL', INTERVAL: 'INTERVAL', //FIXME + INTRATE: 'RENTEFOD', IPMT: 'R.YDELSE', IRR: 'IA', ISBINARY: 'ISBINARY', @@ -184,11 +187,13 @@ const dictionary: RawTranslationPackage = { PRODUCT: 'PRODUKT', POWER: 'POTENS', PPMT: 'H.YDELSE', + PRICEDISC: 'KURS.DISKONTO', PROPER: 'STORT.FORBOGSTAV', PV: 'NV', RADIANS: 'RADIANER', RAND: 'SLUMP', RATE: 'RENTE', + RECEIVED: 'MODTAGET.VED.UDLØB', REPLACE: 'ERSTAT', REPT: 'GENTAG', RIGHT: 'HØJRE', @@ -259,6 +264,7 @@ const dictionary: RawTranslationPackage = { XOR: 'XELLER', YEAR: 'ÅR', YEARFRAC: 'ÅR.BRØK', + YIELDDISC: 'AFKAST.DISKONTO', ROMAN: 'ROMERTAL', ARABIC: 'ARABISK', 'HF.ADD': 'HF.ADD', diff --git a/src/i18n/languages/deDE.ts b/src/i18n/languages/deDE.ts index 4d3d39319b..87d3f1c4e5 100644 --- a/src/i18n/languages/deDE.ts +++ b/src/i18n/languages/deDE.ts @@ -25,6 +25,7 @@ const dictionary: RawTranslationPackage = { 'ARRAY_CONSTRAIN': 'ARRAY_CONSTRAIN', ARRAYFORMULA: 'ARRAYFORMULA', ABS: 'ABS', + ACCRINTM: 'AUFGELZINSF', ACOS: 'ARCCOS', ACOSH: 'ARCCOSHYP', ACOT: 'ARCCOTAN', @@ -96,6 +97,7 @@ const dictionary: RawTranslationPackage = { DECIMAL: 'DEZIMAL', DEGREES: 'GRAD', DELTA: 'DELTA', + DISC: 'DISAGIO', DOLLARDE: 'NOTIERUNGDEZ', DOLLARFR: 'NOTIERUNGBRU', EDATE: 'EDATUM', @@ -124,6 +126,7 @@ const dictionary: RawTranslationPackage = { INDEX: 'INDEX', INT: 'GANZZAHL', INTERVAL: 'INTERVAL', //FIXME + INTRATE: 'ZINSSATZ', IPMT: 'ZINSZ', IRR: 'IKV', ISBINARY: 'ISBINARY', @@ -184,11 +187,13 @@ const dictionary: RawTranslationPackage = { PRODUCT: 'PRODUKT', POWER: 'POTENZ', PPMT: 'KAPZ', + PRICEDISC: 'KURSDISAGIO', PROPER: 'GROSS2', PV: 'BW', RADIANS: 'BOGENMASS', RAND: 'ZUFALLSZAHL', RATE: 'ZINS', + RECEIVED: 'AUSZAHLUNG', REPLACE: 'ERSETZEN', REPT: 'WIEDERHOLEN', RIGHT: 'RECHTS', @@ -259,6 +264,7 @@ const dictionary: RawTranslationPackage = { XOR: 'XODER', YEAR: 'JAHR', YEARFRAC: 'BRTEILJAHRE', + YIELDDISC: 'RENDITEDIS', ROMAN: 'RÖMISCH', ARABIC: 'ARABISCH', 'HF.ADD': 'HF.ADD', diff --git a/src/i18n/languages/enGB.ts b/src/i18n/languages/enGB.ts index d271b732cd..14fc955a11 100644 --- a/src/i18n/languages/enGB.ts +++ b/src/i18n/languages/enGB.ts @@ -25,6 +25,7 @@ const dictionary: RawTranslationPackage = { 'ARRAY_CONSTRAIN': 'ARRAY_CONSTRAIN', ARRAYFORMULA: 'ARRAYFORMULA', ABS: 'ABS', + ACCRINTM: 'ACCRINTM', ACOS: 'ACOS', ACOSH: 'ACOSH', ACOT: 'ACOT', @@ -97,6 +98,7 @@ const dictionary: RawTranslationPackage = { DECIMAL: 'DECIMAL', DEGREES: 'DEGREES', DELTA: 'DELTA', + DISC: 'DISC', DOLLARDE: 'DOLLARDE', DOLLARFR: 'DOLLARFR', EDATE: 'EDATE', @@ -125,6 +127,7 @@ const dictionary: RawTranslationPackage = { INDEX: 'INDEX', INT: 'INT', INTERVAL: 'INTERVAL', + INTRATE: 'INTRATE', IPMT: 'IPMT', IRR: 'IRR', ISBINARY: 'ISBINARY', @@ -185,11 +188,13 @@ const dictionary: RawTranslationPackage = { PRODUCT: 'PRODUCT', POWER: 'POWER', PPMT: 'PPMT', + PRICEDISC: 'PRICEDISC', PROPER: 'PROPER', PV: 'PV', RADIANS: 'RADIANS', RAND: 'RAND', RATE: 'RATE', + RECEIVED: 'RECEIVED', REPLACE: 'REPLACE', REPT: 'REPT', RIGHT: 'RIGHT', @@ -261,6 +266,7 @@ const dictionary: RawTranslationPackage = { XLOOKUP: 'XLOOKUP', YEAR: 'YEAR', YEARFRAC: 'YEARFRAC', + YIELDDISC: 'YIELDDISC', 'HF.ADD': 'HF.ADD', 'HF.CONCAT': 'HF.CONCAT', 'HF.DIVIDE': 'HF.DIVIDE', diff --git a/src/i18n/languages/esES.ts b/src/i18n/languages/esES.ts index 9c6b817b98..c9fddad644 100644 --- a/src/i18n/languages/esES.ts +++ b/src/i18n/languages/esES.ts @@ -25,6 +25,7 @@ export const dictionary: RawTranslationPackage = { 'ARRAY_CONSTRAIN': 'ARRAY_CONSTRAIN', ARRAYFORMULA: 'ARRAYFORMULA', ABS: 'ABS', + ACCRINTM: 'INT.ACUM.V', ACOS: 'ACOS', ACOSH: 'ACOSH', ACOT: 'ACOT', @@ -96,6 +97,7 @@ export const dictionary: RawTranslationPackage = { DECIMAL: 'CONV.DECIMAL', DEGREES: 'GRADOS', DELTA: 'DELTA', + DISC: 'TASA.DESC', DOLLARDE: 'MONEDA.DEC', DOLLARFR: 'MONEDA.FRAC', EDATE: 'FECHA.MES', @@ -124,6 +126,7 @@ export const dictionary: RawTranslationPackage = { INDEX: 'INDICE', INT: 'ENTERO', INTERVAL: 'INTERVAL', //FIXME + INTRATE: 'TASA.INT', IPMT: 'PAGOINT', IRR: 'TIR', ISBINARY: 'ISBINARY', @@ -184,11 +187,13 @@ export const dictionary: RawTranslationPackage = { PRODUCT: 'PRODUCTO', POWER: 'POTENCIA', PPMT: 'PAGOPRIN', + PRICEDISC: 'PRECIO.DESCUENTO', PROPER: 'NOMPROPIO', PV: 'VA', RADIANS: 'RADIANES', RAND: 'ALEATORIO', RATE: 'TASA', + RECEIVED: 'CANTIDAD.RECIBIDA', REPLACE: 'REEMPLAZAR', REPT: 'REPETIR', RIGHT: 'DERECHA', @@ -259,6 +264,7 @@ export const dictionary: RawTranslationPackage = { XOR: 'XOR', YEAR: 'AÑO', YEARFRAC: 'FRAC.AÑO', + YIELDDISC: 'RENDTO.DESC', ROMAN: 'NUMERO.ROMANO', ARABIC: 'ARABIC', 'HF.ADD': 'HF.ADD', diff --git a/src/i18n/languages/fiFI.ts b/src/i18n/languages/fiFI.ts index 99ff05708f..92ae19c978 100644 --- a/src/i18n/languages/fiFI.ts +++ b/src/i18n/languages/fiFI.ts @@ -25,6 +25,7 @@ const dictionary: RawTranslationPackage = { 'ARRAY_CONSTRAIN': 'ARRAY_CONSTRAIN', ARRAYFORMULA: 'ARRAYFORMULA', ABS: 'ITSEISARVO', + ACCRINTM: 'KERTYNYT.KORKO.LOPUSSA', ACOS: 'ACOS', ACOSH: 'ACOSH', ACOT: 'ACOT', @@ -96,6 +97,7 @@ const dictionary: RawTranslationPackage = { DECIMAL: 'DESIMAALI', DEGREES: 'ASTEET', DELTA: 'SAMA.ARVO', + DISC: 'DISKONTTOKORKO', DOLLARDE: 'VALUUTTA.DES', DOLLARFR: 'VALUUTTA.MURTO', EDATE: 'PÄIVÄ.KUUKAUSI', @@ -124,6 +126,7 @@ const dictionary: RawTranslationPackage = { INDEX: 'INDEKSI', INT: 'KOKONAISLUKU', INTERVAL: 'INTERVAL', //FIXME + INTRATE: 'KORKO.ARVOPAPERI', IPMT: 'IPMT', IRR: 'SISÄINEN.KORKO', ISBINARY: 'ISBINARY', @@ -184,11 +187,13 @@ const dictionary: RawTranslationPackage = { PRODUCT: 'TULO', POWER: 'POTENSSI', PPMT: 'PPMT', + PRICEDISC: 'HINTA.DISK', PROPER: 'ERISNIMI', PV: 'NA', RADIANS: 'RADIAANIT', RAND: 'SATUNNAISLUKU', RATE: 'KORKO', + RECEIVED: 'SAATU.HINTA', REPLACE: 'KORVAA', REPT: 'TOISTA', RIGHT: 'OIKEA', @@ -259,6 +264,7 @@ const dictionary: RawTranslationPackage = { XOR: 'EHDOTON.TAI', YEAR: 'VUOSI', YEARFRAC: 'VUOSI.OSA', + YIELDDISC: 'TUOTTO.DISK', ROMAN: 'ROMAN', ARABIC: 'ARABIA', 'HF.ADD': 'HF.ADD', diff --git a/src/i18n/languages/frFR.ts b/src/i18n/languages/frFR.ts index f7b9be45f7..46ddc3341a 100644 --- a/src/i18n/languages/frFR.ts +++ b/src/i18n/languages/frFR.ts @@ -25,6 +25,7 @@ const dictionary: RawTranslationPackage = { 'ARRAY_CONSTRAIN': 'ARRAY_CONSTRAIN', ARRAYFORMULA: 'ARRAYFORMULA', ABS: 'ABS', + ACCRINTM: 'INTERET.ACC.MAT', ACOS: 'ACOS', ACOSH: 'ACOSH', ACOT: 'ACOT', @@ -96,6 +97,7 @@ const dictionary: RawTranslationPackage = { DECIMAL: 'DECIMAL', DEGREES: 'DEGRES', DELTA: 'DELTA', + DISC: 'TAUX.ESCOMPTE', DOLLARDE: 'PRIX.DEC', DOLLARFR: 'PRIX.FRAC', EDATE: 'MOIS.DECALER', @@ -124,6 +126,7 @@ const dictionary: RawTranslationPackage = { INDEX: 'INDEX', INT: 'ENT', INTERVAL: 'INTERVAL', //FIXME + INTRATE: 'TAUX.INTERET', IPMT: 'INTPER', IRR: 'TRI', ISBINARY: 'ISBINARY', @@ -184,11 +187,13 @@ const dictionary: RawTranslationPackage = { PRODUCT: 'PRODUIT', POWER: 'PUISSANCE', PPMT: 'PRINCPER', + PRICEDISC: 'VALEUR.ENCAISSEMENT', PROPER: 'NOMPROPRE', PV: 'VA', RADIANS: 'RADIANS', RAND: 'ALEA', RATE: 'TAUX', + RECEIVED: 'VALEUR.NOMINALE', REPLACE: 'REMPLACER', REPT: 'REPT', RIGHT: 'DROITE', @@ -259,6 +264,7 @@ const dictionary: RawTranslationPackage = { XOR: 'OUX', YEAR: 'ANNEE', YEARFRAC: 'FRACTION.ANNEE', + YIELDDISC: 'RENDEMENT.SIMPLE', ROMAN: 'ROMAIN', ARABIC: 'ARABE', 'HF.ADD': 'HF.ADD', diff --git a/src/i18n/languages/huHU.ts b/src/i18n/languages/huHU.ts index e40219553a..be10141f63 100644 --- a/src/i18n/languages/huHU.ts +++ b/src/i18n/languages/huHU.ts @@ -25,6 +25,7 @@ const dictionary: RawTranslationPackage = { 'ARRAY_CONSTRAIN': 'ARRAY_CONSTRAIN', ARRAYFORMULA: 'ARRAYFORMULA', ABS: 'ABS', + ACCRINTM: 'LEJÁRATI.KAMAT', ACOS: 'ARCCOS', ACOSH: 'ARCCOSH', ACOT: 'ARCCOT', @@ -96,6 +97,7 @@ const dictionary: RawTranslationPackage = { DECIMAL: 'TIZEDES', DEGREES: 'FOK', DELTA: 'DELTA', + DISC: 'LESZÁM', DOLLARDE: 'FORINT.DEC', DOLLARFR: 'FORINT.TÖRT', EDATE: 'KALK.DÁTUM', @@ -124,6 +126,7 @@ const dictionary: RawTranslationPackage = { INDEX: 'INDEX', INT: 'INT', INTERVAL: 'INTERVAL', //FIXME + INTRATE: 'KAMATRÁTA', IPMT: 'RRÉSZLET', IRR: 'BMR', ISBINARY: 'ISBINARY', @@ -184,11 +187,13 @@ const dictionary: RawTranslationPackage = { PRODUCT: 'SZORZAT', POWER: 'HATVÁNY', PPMT: 'PRÉSZLET', + PRICEDISC: 'ÁR.LESZÁM', PROPER: 'TNÉV', PV: 'MÉ', RADIANS: 'RADIÁN', RAND: 'VÉL', RATE: 'RÁTA', + RECEIVED: 'KAPOTT', REPLACE: 'CSERE', REPT: 'SOKSZOR', RIGHT: 'JOBB', @@ -259,6 +264,7 @@ const dictionary: RawTranslationPackage = { XOR: 'XVAGY', YEAR: 'ÉV', YEARFRAC: 'TÖRTÉV', + YIELDDISC: 'HOZAM.LESZÁM', ROMAN: 'RÓMAI', ARABIC: 'ARAB', 'HF.ADD': 'HF.ADD', diff --git a/src/i18n/languages/idID.ts b/src/i18n/languages/idID.ts index 719cb11db6..362748cb91 100644 --- a/src/i18n/languages/idID.ts +++ b/src/i18n/languages/idID.ts @@ -25,6 +25,7 @@ const dictionary: RawTranslationPackage = { 'ARRAY_CONSTRAIN': 'BATASAN.MATRIKS', ARRAYFORMULA: 'RUMUS.MATRIKS', ABS: 'ABS', + ACCRINTM: 'ACCRINTM', ACOS: 'ACOS', ACOSH: 'ACOSH', ACOT: 'ACOT', @@ -97,6 +98,7 @@ const dictionary: RawTranslationPackage = { DECIMAL: 'DESIMAL', DEGREES: 'DERAJAT', DELTA: 'DELTA', + DISC: 'DISC', DOLLARDE: 'DOLAR.DES', DOLLARFR: 'DOLAR.PEC', EDATE: 'EDATE', @@ -125,6 +127,7 @@ const dictionary: RawTranslationPackage = { INDEX: 'INDEKS', INT: 'BULAT.BAWAH', INTERVAL: 'INTERVAL', + INTRATE: 'INTRATE', IPMT: 'IPMT', IRR: 'IRR', ISBINARY: 'ADALAH.BINER', @@ -185,11 +188,13 @@ const dictionary: RawTranslationPackage = { PRODUCT: 'PRODUK', POWER: 'PANGKAT', PPMT: 'PPMT', + PRICEDISC: 'PRICEDISC', PROPER: 'HURUF.AWAL.BESAR', PV: 'PV', RADIANS: 'RADIAN', RAND: 'ACAK', RATE: 'SUKU.BUNGA', + RECEIVED: 'RECEIVED', REPLACE: 'GANTI', REPT: 'ULANG', RIGHT: 'KANAN', @@ -261,6 +266,7 @@ const dictionary: RawTranslationPackage = { XLOOKUP: 'XLOOKUP', YEAR: 'TAHUN', YEARFRAC: 'FRAC.TAHUN', + YIELDDISC: 'YIELDDISC', 'HF.ADD': 'HF.ADD', 'HF.CONCAT': 'HF.CONCAT', 'HF.DIVIDE': 'HF.DIVIDE', diff --git a/src/i18n/languages/itIT.ts b/src/i18n/languages/itIT.ts index 1a494628f8..661d93f61d 100644 --- a/src/i18n/languages/itIT.ts +++ b/src/i18n/languages/itIT.ts @@ -25,6 +25,7 @@ const dictionary: RawTranslationPackage = { 'ARRAY_CONSTRAIN': 'ARRAY_CONSTRAIN', ARRAYFORMULA: 'ARRAYFORMULA', ABS: 'ASS', + ACCRINTM: 'INT.MATURATO.SCAD', ACOS: 'ARCCOS', ACOSH: 'ARCCOSH', ACOT: 'ARCCOT', @@ -96,6 +97,7 @@ const dictionary: RawTranslationPackage = { DECIMAL: 'DECIMALE', DEGREES: 'GRADI', DELTA: 'DELTA', + DISC: 'TASSO.SCONTO', DOLLARDE: 'VALUTA.DEC', DOLLARFR: 'VALUTA.FRAZ', EDATE: 'DATA.MESE', @@ -124,6 +126,7 @@ const dictionary: RawTranslationPackage = { INDEX: 'INDICE', INT: 'INT', INTERVAL: 'INTERVAL', //FIXME + INTRATE: 'TASSO.INT', IPMT: 'INTERESSI', IRR: 'TIR.COST', ISBINARY: 'ISBINARY', @@ -184,11 +187,13 @@ const dictionary: RawTranslationPackage = { PRODUCT: 'PRODOTTO', POWER: 'POTENZA', PPMT: 'P.RATA', + PRICEDISC: 'PREZZO.SCONT', PROPER: 'MAIUSC.INIZ', PV: 'VA', RADIANS: 'RADIANTI', RAND: 'CASUALE', RATE: 'TASSO', + RECEIVED: 'RICEV.SCAD', REPLACE: 'RIMPIAZZA', REPT: 'RIPETI', RIGHT: 'DESTRA', @@ -259,6 +264,7 @@ const dictionary: RawTranslationPackage = { XOR: 'XOR', YEAR: 'ANNO', YEARFRAC: 'FRAZIONE.ANNO', + YIELDDISC: 'REND.TITOLI.SCONT', ROMAN: 'ROMANO', ARABIC: 'ARABO', 'HF.ADD': 'HF.ADD', diff --git a/src/i18n/languages/nbNO.ts b/src/i18n/languages/nbNO.ts index ff15415a98..f3049f460a 100644 --- a/src/i18n/languages/nbNO.ts +++ b/src/i18n/languages/nbNO.ts @@ -25,6 +25,7 @@ const dictionary: RawTranslationPackage = { 'ARRAY_CONSTRAIN': 'ARRAY_CONSTRAIN', ARRAYFORMULA: 'ARRAYFORMULA', ABS: 'ABS', + ACCRINTM: 'PÅLØPT.FORFALLSRENTE', ACOS: 'ARCCOS', ACOSH: 'ARCCOSH', ACOT: 'ACOT', @@ -96,6 +97,7 @@ const dictionary: RawTranslationPackage = { DECIMAL: 'DESIMAL', DEGREES: 'GRADER', DELTA: 'DELTA', + DISC: 'DISKONTERT', DOLLARDE: 'DOLLARDE', DOLLARFR: 'DOLLARBR', EDATE: 'DAG.ETTER', @@ -124,6 +126,7 @@ const dictionary: RawTranslationPackage = { INDEX: 'INDEKS', INT: 'HELTALL', INTERVAL: 'INTERVAL', //FIXME + INTRATE: 'RENTESATS', IPMT: 'RAVDRAG', IRR: 'IR', ISBINARY: 'ISBINARY', @@ -184,11 +187,13 @@ const dictionary: RawTranslationPackage = { PRODUCT: 'PRODUKT', POWER: 'OPPHØYD.I', PPMT: 'AMORT', + PRICEDISC: 'PRIS.DISKONTERT', PROPER: 'STOR.FORBOKSTAV', PV: 'NÅVERDI', RADIANS: 'RADIANER', RAND: 'TILFELDIG', RATE: 'RENTE', + RECEIVED: 'MOTTATT.AVKAST', REPLACE: 'ERSTATT', REPT: 'GJENTA', RIGHT: 'HØYRE', @@ -259,6 +264,7 @@ const dictionary: RawTranslationPackage = { XOR: 'EKSKLUSIVELLER', YEAR: 'ÅR', YEARFRAC: 'ÅRDEL', + YIELDDISC: 'AVKAST.DISKONTERT', ROMAN: 'ROMERTALL', ARABIC: 'ARABISK', 'HF.ADD': 'HF.ADD', diff --git a/src/i18n/languages/nlNL.ts b/src/i18n/languages/nlNL.ts index c491e2f44b..fa876c35df 100644 --- a/src/i18n/languages/nlNL.ts +++ b/src/i18n/languages/nlNL.ts @@ -25,6 +25,7 @@ const dictionary: RawTranslationPackage = { 'ARRAY_CONSTRAIN': 'ARRAY_CONSTRAIN', ARRAYFORMULA: 'ARRAYFORMULA', ABS: 'ABS', + ACCRINTM: 'SAMENG.RENTE.V', ACOS: 'BOOGCOS', ACOSH: 'BOOGCOSH', ACOT: 'BOOGCOT', @@ -96,6 +97,7 @@ const dictionary: RawTranslationPackage = { DECIMAL: 'DECIMAAL', DEGREES: 'GRADEN', DELTA: 'DELTA', + DISC: 'DISCONTO', DOLLARDE: 'EURO.DE', DOLLARFR: 'EURO.BR', EDATE: 'ZELFDE.DAG', @@ -124,6 +126,7 @@ const dictionary: RawTranslationPackage = { INDEX: 'INDEX', INT: 'INTEGER', INTERVAL: 'INTERVAL', //FIXME + INTRATE: 'RENTEPERCENTAGE', IPMT: 'IBET', IRR: 'IR', ISBINARY: 'ISBINARY', @@ -184,11 +187,13 @@ const dictionary: RawTranslationPackage = { PRODUCT: 'PRODUCT', POWER: 'MACHT', PPMT: 'PBET', + PRICEDISC: 'PRIJS.DISCONTO', PROPER: 'BEGINLETTERS', PV: 'HW', RADIANS: 'RADIALEN', RAND: 'ASELECT', RATE: 'RENTE', + RECEIVED: 'OPBRENGST', REPLACE: 'VERVANGEN', REPT: 'HERHALING', RIGHT: 'RECHTS', @@ -259,6 +264,7 @@ const dictionary: RawTranslationPackage = { XOR: 'EX.OF', YEAR: 'JAAR', YEARFRAC: 'JAAR.DEEL', + YIELDDISC: 'REND.DISCONTO', ROMAN: 'ROMEINS', ARABIC: 'ARABISCH', 'HF.ADD': 'HF.ADD', diff --git a/src/i18n/languages/plPL.ts b/src/i18n/languages/plPL.ts index 6fa4f016b4..73942b01c9 100644 --- a/src/i18n/languages/plPL.ts +++ b/src/i18n/languages/plPL.ts @@ -25,6 +25,7 @@ const dictionary: RawTranslationPackage = { 'ARRAY_CONSTRAIN': 'ARRAY_CONSTRAIN', ARRAYFORMULA: 'ARRAYFORMULA', ABS: 'WART.BEZWGL', + ACCRINTM: 'NAL.ODS.WYKUP', ACOS: 'ACOS', ACOSH: 'ACOSH', ACOT: 'ACOT', @@ -96,6 +97,7 @@ const dictionary: RawTranslationPackage = { DECIMAL: 'DZIESIĘTNA', DEGREES: 'STOPNIE', DELTA: 'DELTA', + DISC: 'STOPA.DYSK', DOLLARDE: 'CENA.DZIES', DOLLARFR: 'CENA.UŁAM', EDATE: 'NR.SER.DATY', @@ -124,6 +126,7 @@ const dictionary: RawTranslationPackage = { INDEX: 'INDEKS', INT: 'ZAOKR.DO.CAŁK', INTERVAL: 'INTERVAL', //FIXME + INTRATE: 'STOPA.PROC', IPMT: 'IPMT', IRR: 'IRR', ISBINARY: 'ISBINARY', @@ -184,11 +187,13 @@ const dictionary: RawTranslationPackage = { PRODUCT: 'ILOCZYN', POWER: 'POTĘGA', PPMT: 'PPMT', + PRICEDISC: 'CENA.DYSK', PROPER: 'Z.WIELKIEJ.LITERY', PV: 'PV', RADIANS: 'RADIANY', RAND: 'LOSUJ', RATE: 'RATE', + RECEIVED: 'KWOTA.WYKUP', REPLACE: 'ZASTĄP', REPT: 'POWT', RIGHT: 'PRAWY', @@ -259,6 +264,7 @@ const dictionary: RawTranslationPackage = { XOR: 'XOR', YEAR: 'ROK', YEARFRAC: 'CZĘŚĆ.ROKU', + YIELDDISC: 'RENT.DYSK', ROMAN: 'RZYMSKIE', ARABIC: 'ARABSKA', 'HF.ADD': 'HF.ADD', diff --git a/src/i18n/languages/ptPT.ts b/src/i18n/languages/ptPT.ts index 6c1e68c328..3928f321a1 100644 --- a/src/i18n/languages/ptPT.ts +++ b/src/i18n/languages/ptPT.ts @@ -25,6 +25,7 @@ const dictionary: RawTranslationPackage = { 'ARRAY_CONSTRAIN': 'ARRAY_CONSTRAIN', ARRAYFORMULA: 'ARRAYFORMULA', ABS: 'ABS', + ACCRINTM: 'JUROSACUMV', ACOS: 'ACOS', ACOSH: 'ACOSH', ACOT: 'ACOT', @@ -96,6 +97,7 @@ const dictionary: RawTranslationPackage = { DECIMAL: 'DECIMAL', DEGREES: 'GRAUS', DELTA: 'DELTA', + DISC: 'DESC', DOLLARDE: 'MOEDADEC', DOLLARFR: 'MOEDAFRA', EDATE: 'DATAM', @@ -124,6 +126,7 @@ const dictionary: RawTranslationPackage = { INDEX: 'ÍNDICE', INT: 'INT', INTERVAL: 'INTERVAL', //FIXME + INTRATE: 'TAXAJUROS', IPMT: 'IPGTO', IRR: 'TIR', ISBINARY: 'ISBINARY', @@ -184,11 +187,13 @@ const dictionary: RawTranslationPackage = { PRODUCT: 'MULT', POWER: 'POTÊNCIA', PPMT: 'PPGTO', + PRICEDISC: 'PREÇODESC', PROPER: 'PRI.MAIÚSCULA', PV: 'VP', RADIANS: 'RADIANOS', RAND: 'ALEATÓRIO', RATE: 'TAXA', + RECEIVED: 'RECEBER', REPLACE: 'MUDAR', REPT: 'REPT', RIGHT: 'DIREITA', @@ -259,6 +264,7 @@ const dictionary: RawTranslationPackage = { XOR: 'OUEXCL', YEAR: 'ANO', YEARFRAC: 'FRAÇÃOANO', + YIELDDISC: 'LUCRODESC', ROMAN: 'ROMANO', ARABIC: 'CARDINAL', 'HF.ADD': 'HF.ADD', diff --git a/src/i18n/languages/ruRU.ts b/src/i18n/languages/ruRU.ts index 6b35c1ce2f..17f8d6e854 100644 --- a/src/i18n/languages/ruRU.ts +++ b/src/i18n/languages/ruRU.ts @@ -25,6 +25,7 @@ const dictionary: RawTranslationPackage = { 'ARRAY_CONSTRAIN': 'ARRAY_CONSTRAIN', ARRAYFORMULA: 'ARRAYFORMULA', ABS: 'ABS', + ACCRINTM: 'НАКОПДОХОДПОГАШ', ACOS: 'ACOS', ACOSH: 'ACOSH', ACOT: 'ACOT', @@ -96,6 +97,7 @@ const dictionary: RawTranslationPackage = { DECIMAL: 'ДЕС', DEGREES: 'ГРАДУСЫ', DELTA: 'ДЕЛЬТА', + DISC: 'СКИДКА', DOLLARDE: 'РУБЛЬ.ДЕС', DOLLARFR: 'РУБЛЬ.ДРОБЬ', EDATE: 'ДАТАМЕС', @@ -124,6 +126,7 @@ const dictionary: RawTranslationPackage = { INDEX: 'ИНДЕКС', INT: 'ЦЕЛОЕ', INTERVAL: 'INTERVAL', //FIXME + INTRATE: 'ИНОРМА', IPMT: 'ПРПЛТ', IRR: 'ВСД', ISBINARY: 'ISBINARY', @@ -184,11 +187,13 @@ const dictionary: RawTranslationPackage = { PRODUCT: 'ПРОИЗВЕД', POWER: 'СТЕПЕНЬ', PPMT: 'ОСПЛТ', + PRICEDISC: 'ЦЕНАСКИДКА', PROPER: 'ПРОПНАЧ', PV: 'ПС', RADIANS: 'РАДИАНЫ', RAND: 'СЛЧИС', RATE: 'СТАВКА', + RECEIVED: 'ПОЛУЧЕНО', REPLACE: 'ЗАМЕНИТЬ', REPT: 'ПОВТОР', RIGHT: 'ПРАВСИМВ', @@ -259,6 +264,7 @@ const dictionary: RawTranslationPackage = { XOR: 'ИСКЛИЛИ', YEAR: 'ГОД', YEARFRAC: 'ДОЛЯГОДА', + YIELDDISC: 'ДОХОДСКИДКА', ROMAN: 'РИМСКОЕ', ARABIC: 'АРАБСКОЕ', 'HF.ADD': 'HF.ADD', diff --git a/src/i18n/languages/svSE.ts b/src/i18n/languages/svSE.ts index 5f87580087..21ac96033f 100644 --- a/src/i18n/languages/svSE.ts +++ b/src/i18n/languages/svSE.ts @@ -25,6 +25,7 @@ const dictionary: RawTranslationPackage = { 'ARRAY_CONSTRAIN': 'ARRAY_CONSTRAIN', ARRAYFORMULA: 'ARRAYFORMULA', ABS: 'ABS', + ACCRINTM: 'UPPLOBLRÄNTA', ACOS: 'ARCCOS', ACOSH: 'ARCCOSH', ACOT: 'ACOT', @@ -96,6 +97,7 @@ const dictionary: RawTranslationPackage = { DECIMAL: 'DECIMAL', DEGREES: 'GRADER', DELTA: 'DELTA', + DISC: 'DISK', DOLLARDE: 'DECTAL', DOLLARFR: 'BRÅK', EDATE: 'EDATUM', @@ -124,6 +126,7 @@ const dictionary: RawTranslationPackage = { INDEX: 'INDEX', INT: 'HELTAL', INTERVAL: 'INTERVAL', //FIXME + INTRATE: 'ÅRSRÄNTA', IPMT: 'RBETALNING', IRR: 'IR', ISBINARY: 'ISBINARY', @@ -184,11 +187,13 @@ const dictionary: RawTranslationPackage = { PRODUCT: 'PRODUKT', POWER: 'UPPHÖJT.TILL', PPMT: 'AMORT', + PRICEDISC: 'PRISDISK', PROPER: 'INITIAL', PV: 'NUVÄRDE', RADIANS: 'RADIANER', RAND: 'SLUMP', RATE: 'RÄNTA', + RECEIVED: 'BELOPP', REPLACE: 'ERSÄTT', REPT: 'REP', RIGHT: 'HÖGER', @@ -259,6 +264,7 @@ const dictionary: RawTranslationPackage = { XOR: 'XOR', YEAR: 'ÅR', YEARFRAC: 'ÅRDEL', + YIELDDISC: 'NOMAVKDISK', ROMAN: 'ROMERSK', ARABIC: 'ARABIC', 'HF.ADD': 'HF.ADD', diff --git a/src/i18n/languages/trTR.ts b/src/i18n/languages/trTR.ts index 93c2176d37..dc5a53cbb2 100644 --- a/src/i18n/languages/trTR.ts +++ b/src/i18n/languages/trTR.ts @@ -25,6 +25,7 @@ const dictionary: RawTranslationPackage = { 'ARRAY_CONSTRAIN': 'ARRAY_CONSTRAIN', ARRAYFORMULA: 'ARRAYFORMULA', ABS: 'MUTLAK', + ACCRINTM: 'GERÇEKFAİZV', ACOS: 'ACOS', ACOSH: 'ACOSH', ACOT: 'ACOT', @@ -96,6 +97,7 @@ const dictionary: RawTranslationPackage = { DECIMAL: 'ONDALIK', DEGREES: 'DERECE', DELTA: 'DELTA', + DISC: 'İNDİRİM', DOLLARDE: 'LİRAON', DOLLARFR: 'LİRAKES', EDATE: 'SERİTARİH', @@ -124,6 +126,7 @@ const dictionary: RawTranslationPackage = { INDEX: 'İNDİS', INT: 'TAMSAYI', INTERVAL: 'INTERVAL', //FIXME + INTRATE: 'FAİZORANI', IPMT: 'FAİZTUTARI', IRR: 'İÇ_VERİM_ORANI', ISBINARY: 'ISBINARY', @@ -184,11 +187,13 @@ const dictionary: RawTranslationPackage = { PRODUCT: 'ÇARPIM', POWER: 'KUVVET', PPMT: 'ANA_PARA_ÖDEMESİ', + PRICEDISC: 'DEĞERİND', PROPER: 'YAZIM.DÜZENİ', PV: 'BD', RADIANS: 'RADYAN', RAND: 'S_SAYI_ÜRET', RATE: 'FAİZ_ORANI', + RECEIVED: 'GETİRİ', REPLACE: 'DEĞİŞTİR', REPT: 'YİNELE', RIGHT: 'SAĞ', @@ -259,6 +264,7 @@ const dictionary: RawTranslationPackage = { XOR: 'ÖZELVEYA', YEAR: 'YIL', YEARFRAC: 'YILORAN', + YIELDDISC: 'ÖDEMEİND', ROMAN: 'ROMEN', ARABIC: 'ARAPÇA', 'HF.ADD': 'HF.ADD', diff --git a/src/interpreter/functionMetadata/categories/financial.ts b/src/interpreter/functionMetadata/categories/financial.ts index f673c11953..febe1ab7c7 100644 --- a/src/interpreter/functionMetadata/categories/financial.ts +++ b/src/interpreter/functionMetadata/categories/financial.ts @@ -10,6 +10,13 @@ import {FunctionDoc} from '../FunctionDescription' * truth for the function metadata API, and `docs/guide/built-in-functions.md` is generated from it. */ export const FINANCIAL_DOCS: Record = { + ACCRINTM: { + category: 'Financial', + shortDescription: 'Returns the accrued interest of a security that pays interest at maturity.', + parameters: [{name: 'issue', description: 'The issue date, as a date serial number, truncated to an integer.'}, {name: 'settlement', description: 'The settlement date, as a date serial number, truncated to an integer; must not be earlier than `issue` (equal dates give 0).'}, {name: 'rate', description: 'The annual coupon rate; must be greater than 0.'}, {name: 'par', description: 'The par value; must be greater than 0. If the argument is omitted or empty, 1000 is used.'}, {name: 'basis', description: 'The day-count basis, truncated to an integer: 0 (default) US 30/360, 1 actual/actual, 2 actual/360, 3 actual/365, 4 European 30/360.'}], + documentationUrl: 'https://hyperformula.handsontable.com/docs/guide/built-in-functions.html', + examples: ['=ACCRINTM(DATE(2008,4,1), DATE(2008,6,15), 0.1, 1000, 3)', '=ACCRINTM(A1, B1, 0.05)'], + }, CUMIPMT: { category: 'Financial', shortDescription: 'Returns the cumulative interest paid on a loan between a start period and an end period.', @@ -38,6 +45,13 @@ export const FINANCIAL_DOCS: Record = { documentationUrl: 'https://hyperformula.handsontable.com/docs/guide/built-in-functions.html', examples: ['=DDB(10000, 1000, 6, 2)', '=DDB(10000, 1000, 6, 1, 1.5)'], }, + DISC: { + category: 'Financial', + shortDescription: 'Returns the discount rate of a security.', + parameters: [{name: 'settlement', description: 'The settlement date, as a date serial number, truncated to an integer.'}, {name: 'maturity', description: 'The maturity date, as a date serial number, truncated to an integer; must be later than `settlement`.'}, {name: 'pr', description: 'The price per $100 face value; must be greater than 0.'}, {name: 'redemption', description: 'The redemption value per $100 face value; must be greater than 0.'}, {name: 'basis', description: 'The day-count basis, truncated to an integer: 0 (default) US 30/360, 1 actual/actual, 2 actual/360, 3 actual/365, 4 European 30/360.'}], + documentationUrl: 'https://hyperformula.handsontable.com/docs/guide/built-in-functions.html', + examples: ['=DISC(DATE(2018,7,1), DATE(2048,1,1), 97.975, 100, 1)', '=DISC(A1, B1, 99, 100)'], + }, DOLLARDE: { category: 'Financial', shortDescription: 'Converts a price entered with a special notation to a price displayed as a decimal number.', @@ -73,6 +87,13 @@ export const FINANCIAL_DOCS: Record = { documentationUrl: 'https://hyperformula.handsontable.com/docs/guide/built-in-functions.html', examples: ['=FVSCHEDULE(1000, A1:A3)', '=FVSCHEDULE(5000, B2:B5)'], }, + INTRATE: { + category: 'Financial', + shortDescription: 'Returns the interest rate of a fully invested security.', + parameters: [{name: 'settlement', description: 'The settlement date, as a date serial number, truncated to an integer.'}, {name: 'maturity', description: 'The maturity date, as a date serial number, truncated to an integer; must be later than `settlement`.'}, {name: 'investment', description: 'The amount invested; must be greater than 0.'}, {name: 'redemption', description: 'The amount received at `maturity`; must be greater than 0.'}, {name: 'basis', description: 'The day-count basis, truncated to an integer: 0 (default) US 30/360, 1 actual/actual, 2 actual/360, 3 actual/365, 4 European 30/360.'}], + documentationUrl: 'https://hyperformula.handsontable.com/docs/guide/built-in-functions.html', + examples: ['=INTRATE(DATE(2008,2,15), DATE(2008,5,15), 1000000, 1014420, 2)', '=INTRATE(A1, B1, 1000, 1050)'], + }, IPMT: { category: 'Financial', shortDescription: 'Returns the interest portion of a given loan payment in a given payment period.', @@ -143,6 +164,13 @@ export const FINANCIAL_DOCS: Record = { documentationUrl: 'https://hyperformula.handsontable.com/docs/guide/built-in-functions.html', examples: ['=PPMT(0.05/12, 1, 60, 20000)', '=PPMT(0.04/12, 12, 360, 200000, 0, 1)'], }, + PRICEDISC: { + category: 'Financial', + shortDescription: 'Returns the price per $100 face value of a discounted security.', + parameters: [{name: 'settlement', description: 'The settlement date, as a date serial number, truncated to an integer.'}, {name: 'maturity', description: 'The maturity date, as a date serial number, truncated to an integer; must be later than `settlement`.'}, {name: 'discount', description: 'The discount rate; must be greater than 0. A large `discount` gives a zero or negative price.'}, {name: 'redemption', description: 'The redemption value per $100 face value; must be greater than 0.'}, {name: 'basis', description: 'The day-count basis, truncated to an integer: 0 (default) US 30/360, 1 actual/actual, 2 actual/360, 3 actual/365, 4 European 30/360.'}], + documentationUrl: 'https://hyperformula.handsontable.com/docs/guide/built-in-functions.html', + examples: ['=PRICEDISC(DATE(2008,2,16), DATE(2008,3,1), 0.0525, 100, 2)', '=PRICEDISC(A1, B1, 0.05, 100)'], + }, PV: { category: 'Financial', shortDescription: 'Returns the present value of an investment.', @@ -157,6 +185,13 @@ export const FINANCIAL_DOCS: Record = { documentationUrl: 'https://hyperformula.handsontable.com/docs/guide/built-in-functions.html', examples: ['=RATE(60, -100, 5000)', '=RATE(360, -1000, 200000, 0, 0, 0.05)'], }, + RECEIVED: { + category: 'Financial', + shortDescription: 'Returns the amount received at maturity for a fully invested security.', + parameters: [{name: 'settlement', description: 'The settlement date, as a date serial number, truncated to an integer.'}, {name: 'maturity', description: 'The maturity date, as a date serial number, truncated to an integer; must be later than `settlement`.'}, {name: 'investment', description: 'The amount invested; must be greater than 0.'}, {name: 'discount', description: 'The discount rate; must be greater than 0. If `discount` multiplied by the year fraction from `settlement` to `maturity` is 1 or more, the result is #NUM!.'}, {name: 'basis', description: 'The day-count basis, truncated to an integer: 0 (default) US 30/360, 1 actual/actual, 2 actual/360, 3 actual/365, 4 European 30/360.'}], + documentationUrl: 'https://hyperformula.handsontable.com/docs/guide/built-in-functions.html', + examples: ['=RECEIVED(DATE(2008,2,15), DATE(2008,5,15), 1000000, 0.0575, 2)', '=RECEIVED(A1, B1, 1000, 0.05)'], + }, RRI: { category: 'Financial', shortDescription: 'Returns an equivalent interest rate for the growth of an investment.', @@ -213,4 +248,11 @@ export const FINANCIAL_DOCS: Record = { documentationUrl: 'https://hyperformula.handsontable.com/docs/guide/built-in-functions.html', examples: ['=XIRR(A1:A4, B1:B4)', '=XIRR(A1:A5, B1:B5, 0.1)'], }, + YIELDDISC: { + category: 'Financial', + shortDescription: 'Returns the annual yield of a discounted security.', + parameters: [{name: 'settlement', description: 'The settlement date, as a date serial number, truncated to an integer.'}, {name: 'maturity', description: 'The maturity date, as a date serial number, truncated to an integer; must be later than `settlement`.'}, {name: 'pr', description: 'The price per $100 face value; must be greater than 0.'}, {name: 'redemption', description: 'The redemption value per $100 face value; must be greater than 0.'}, {name: 'basis', description: 'The day-count basis, truncated to an integer: 0 (default) US 30/360, 1 actual/actual, 2 actual/360, 3 actual/365, 4 European 30/360.'}], + documentationUrl: 'https://hyperformula.handsontable.com/docs/guide/built-in-functions.html', + examples: ['=YIELDDISC(DATE(2008,2,16), DATE(2008,3,1), 99.795, 100, 2)', '=YIELDDISC(A1, B1, 99, 100)'], + }, } diff --git a/src/interpreter/plugin/FinancialPlugin.ts b/src/interpreter/plugin/FinancialPlugin.ts index c1f0c782a8..1a0c28b3dd 100644 --- a/src/interpreter/plugin/FinancialPlugin.ts +++ b/src/interpreter/plugin/FinancialPlugin.ts @@ -240,6 +240,66 @@ export class FinancialPlugin extends FunctionPlugin implements FunctionPluginTyp ], returnNumberType: NumberType.NUMBER_PERCENT }, + 'DISC': { + method: 'disc', + parameters: [ + {argumentType: FunctionArgumentType.SCALAR}, + {argumentType: FunctionArgumentType.SCALAR}, + {argumentType: FunctionArgumentType.SCALAR}, + {argumentType: FunctionArgumentType.SCALAR}, + {argumentType: FunctionArgumentType.SCALAR, defaultValue: 0}, + ], + }, + 'INTRATE': { + method: 'intrate', + parameters: [ + {argumentType: FunctionArgumentType.SCALAR}, + {argumentType: FunctionArgumentType.SCALAR}, + {argumentType: FunctionArgumentType.SCALAR}, + {argumentType: FunctionArgumentType.SCALAR}, + {argumentType: FunctionArgumentType.SCALAR, defaultValue: 0}, + ], + }, + 'PRICEDISC': { + method: 'pricedisc', + parameters: [ + {argumentType: FunctionArgumentType.SCALAR}, + {argumentType: FunctionArgumentType.SCALAR}, + {argumentType: FunctionArgumentType.SCALAR}, + {argumentType: FunctionArgumentType.SCALAR}, + {argumentType: FunctionArgumentType.SCALAR, defaultValue: 0}, + ], + }, + 'RECEIVED': { + method: 'received', + parameters: [ + {argumentType: FunctionArgumentType.SCALAR}, + {argumentType: FunctionArgumentType.SCALAR}, + {argumentType: FunctionArgumentType.SCALAR}, + {argumentType: FunctionArgumentType.SCALAR}, + {argumentType: FunctionArgumentType.SCALAR, defaultValue: 0}, + ], + }, + 'YIELDDISC': { + method: 'yielddisc', + parameters: [ + {argumentType: FunctionArgumentType.SCALAR}, + {argumentType: FunctionArgumentType.SCALAR}, + {argumentType: FunctionArgumentType.SCALAR}, + {argumentType: FunctionArgumentType.SCALAR}, + {argumentType: FunctionArgumentType.SCALAR, defaultValue: 0}, + ], + }, + 'ACCRINTM': { + method: 'accrintm', + parameters: [ + {argumentType: FunctionArgumentType.SCALAR}, + {argumentType: FunctionArgumentType.SCALAR}, + {argumentType: FunctionArgumentType.SCALAR}, + {argumentType: FunctionArgumentType.SCALAR, defaultValue: 1000, emptyAsDefault: true}, + {argumentType: FunctionArgumentType.SCALAR, defaultValue: 0}, + ], + }, 'FVSCHEDULE': { method: 'fvschedule', parameters: [ @@ -649,6 +709,128 @@ export class FinancialPlugin extends FunctionPlugin implements FunctionPluginTyp ) } + /** + * Corresponds to DISC(settlement, maturity, pr, redemption, [basis]). + * + * Returns the discount rate of a security. + * + * @param ast + * @param state + */ + public disc(ast: ProcedureAst, state: InterpreterState): InterpreterValue { + return this.discountedSecurity(ast, state, 'DISC', + (dayCount: number, yearDays: number, price: number, redemption: number) => (1 - price / redemption) * (yearDays / dayCount) + ) + } + + /** + * Corresponds to INTRATE(settlement, maturity, investment, redemption, [basis]). + * + * Returns the interest rate of a fully invested security. + * + * @param ast + * @param state + */ + public intrate(ast: ProcedureAst, state: InterpreterState): InterpreterValue { + return this.discountedSecurity(ast, state, 'INTRATE', + (dayCount: number, yearDays: number, investment: number, redemption: number) => (redemption - investment) / investment * (yearDays / dayCount) + ) + } + + /** + * Corresponds to PRICEDISC(settlement, maturity, discount, redemption, [basis]). + * + * Returns the price per $100 face value of a discounted security. + * + * @param ast + * @param state + */ + public pricedisc(ast: ProcedureAst, state: InterpreterState): InterpreterValue { + return this.discountedSecurity(ast, state, 'PRICEDISC', + (dayCount: number, yearDays: number, discount: number, redemption: number) => redemption - discount * redemption * (dayCount / yearDays) + ) + } + + /** + * Corresponds to RECEIVED(settlement, maturity, investment, discount, [basis]). + * + * Returns the amount received at maturity for a fully invested security. + * Returns #NUM! when the discount for the whole period reaches the investment, that is when + * `discount * dayCount / yearDays` is 1 or more. + * + * @param ast + * @param state + */ + public received(ast: ProcedureAst, state: InterpreterState): InterpreterValue { + return this.discountedSecurity(ast, state, 'RECEIVED', + (dayCount: number, yearDays: number, investment: number, discount: number) => { + const denominator = 1 - discount * (dayCount / yearDays) + if (denominator <= 0) { + return new CellError(ErrorType.NUM, ErrorMessage.ValueLarge) + } + return investment / denominator + } + ) + } + + /** + * Corresponds to YIELDDISC(settlement, maturity, pr, redemption, [basis]). + * + * Returns the annual yield of a discounted security. + * + * @param ast + * @param state + */ + public yielddisc(ast: ProcedureAst, state: InterpreterState): InterpreterValue { + return this.discountedSecurity(ast, state, 'YIELDDISC', + (dayCount: number, yearDays: number, price: number, redemption: number) => (redemption - price) / price * (yearDays / dayCount) + ) + } + + /** + * Corresponds to ACCRINTM(issue, settlement, rate, [par], [basis]). + * + * Returns the accrued interest of a security that pays interest at maturity. An empty or omitted `par` is 1000. + * Arguments are validated from left to right, as in Excel. `issue` equal to `settlement` gives 0. + * + * @param ast + * @param state + */ + public accrintm(ast: ProcedureAst, state: InterpreterState): InterpreterValue { + return this.runFunction(ast.args, state, this.metadata('ACCRINTM'), + (issueArg: InternalScalarValue, settlementArg: InternalScalarValue, rateArg: InternalScalarValue, parArg: InternalScalarValue, basisArg: InternalScalarValue) => { + const issue = this.coerceToSecurityDate(issueArg) + if (issue instanceof CellError) { + return issue + } + const settlement = this.coerceToSecurityDate(settlementArg) + if (settlement instanceof CellError) { + return settlement + } + const rate = this.strictNumber(rateArg) + if (rate instanceof CellError) { + return rate + } + const par = this.strictNumber(parArg) + if (par instanceof CellError) { + return par + } + const basis = this.coerceToDayCountBasis(basisArg) + if (basis instanceof CellError) { + return basis + } + if (issue > settlement) { + return new CellError(ErrorType.NUM, ErrorMessage.StartEndDate) + } + if (rate <= 0 || par <= 0) { + return new CellError(ErrorType.NUM, ErrorMessage.ValueSmall) + } + const {dayCount, yearDays} = this.dateTimeHelper.dayCountByBasis(issue, settlement, basis) + return par * rate * (dayCount / yearDays) + } + ) + } + public fvschedule(ast: ProcedureAst, state: InterpreterState): InterpreterValue { return this.runFunction(ast.args, state, this.metadata('FVSCHEDULE'), (value: number, ratios: SimpleRangeValue) => { @@ -841,6 +1023,108 @@ export class FinancialPlugin extends FunctionPlugin implements FunctionPluginTyp } ) } + + /** + * Runs DISC, INTRATE, PRICEDISC, RECEIVED and YIELDDISC, which share the arguments + * (settlement, maturity, first amount, second amount, [basis]). + * + * Validates in Excel's order: settlement and maturity, then basis, then the two amounts (errors and #VALUE!), + * then settlement before maturity and both amounts positive (#NUM!). Passes the day count and the days in the year + * between settlement and maturity to `calculate`. + * + * @param {ProcedureAst} ast - the function's AST + * @param {InterpreterState} state - the interpreter state + * @param {string} functionName - the function's id, used to look up its metadata + * @param {Function} calculate - computes the result from the day count, the days in the year and the two amounts + */ + private discountedSecurity( + ast: ProcedureAst, + state: InterpreterState, + functionName: string, + calculate: (dayCount: number, yearDays: number, firstAmount: number, secondAmount: number) => number | CellError, + ): InterpreterValue { + return this.runFunction(ast.args, state, this.metadata(functionName), + (settlementArg: InternalScalarValue, maturityArg: InternalScalarValue, firstAmountArg: InternalScalarValue, secondAmountArg: InternalScalarValue, basisArg: InternalScalarValue) => { + const settlement = this.coerceToSecurityDate(settlementArg) + if (settlement instanceof CellError) { + return settlement + } + const maturity = this.coerceToSecurityDate(maturityArg) + if (maturity instanceof CellError) { + return maturity + } + const basis = this.coerceToDayCountBasis(basisArg) + if (basis instanceof CellError) { + return basis + } + const firstAmount = this.strictNumber(firstAmountArg) + if (firstAmount instanceof CellError) { + return firstAmount + } + const secondAmount = this.strictNumber(secondAmountArg) + if (secondAmount instanceof CellError) { + return secondAmount + } + if (settlement >= maturity) { + return new CellError(ErrorType.NUM, ErrorMessage.StartEndDate) + } + if (firstAmount <= 0 || secondAmount <= 0) { + return new CellError(ErrorType.NUM, ErrorMessage.ValueSmall) + } + const {dayCount, yearDays} = this.dateTimeHelper.dayCountByBasis(settlement, maturity, basis) + return calculate(dayCount, yearDays, firstAmount, secondAmount) + } + ) + } + + /** + * Converts a date argument of a securities function: a number as `strictNumber` accepts it, truncated to an integer. + * A date outside the supported range is #NUM!. + */ + private coerceToSecurityDate(value: InternalScalarValue): number | CellError { + const dateNumber = this.strictNumber(value) + if (dateNumber instanceof CellError) { + return dateNumber + } + const date = Math.trunc(dateNumber) + if (this.dateTimeHelper.getWithinBounds(date) === undefined) { + return new CellError(ErrorType.NUM, ErrorMessage.DateBounds) + } + return date + } + + /** + * Converts the day-count `basis` argument of a securities function: a number as `strictNumber` accepts it, from 0 + * to less than 5, truncated to an integer. A negative value is #NUM! even when it truncates to 0, as in Excel. + */ + private coerceToDayCountBasis(value: InternalScalarValue): number | CellError { + const basis = this.strictNumber(value) + if (basis instanceof CellError) { + return basis + } + if (basis < 0) { + return new CellError(ErrorType.NUM, ErrorMessage.ValueSmall) + } + if (basis >= 5) { + return new CellError(ErrorType.NUM, ErrorMessage.ValueLarge) + } + return Math.trunc(basis) + } + + /** + * Converts an argument the way Excel does for these functions: numbers, dates, numeric text and empty cells are + * accepted, while a boolean or the empty text is #VALUE!. Errors are passed on. + */ + private strictNumber(value: InternalScalarValue): number | CellError { + if (value instanceof CellError) { + return value + } + if (typeof value === 'boolean' || value === '') { + return new CellError(ErrorType.VALUE, ErrorMessage.NumberCoercion) + } + const coerced = this.coerceScalarToNumberOrError(value) + return coerced instanceof CellError ? coerced : getRawValue(coerced) + } } function pmtCore(rate: number, periods: number, present: number, future: number, type: number): number { From b3989ed1dda5bc4640392079e85f04d75fc6fb85 Mon Sep 17 00:00:00 2001 From: tobiadefami Date: Wed, 7 Oct 2026 22:58:31 +0100 Subject: [PATCH 3/4] Link the pull request in the changelog Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 985f015c97..cd0af84707 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Added -- Added new functions: ACCRINTM, DISC, INTRATE, PRICEDISC, RECEIVED, YIELDDISC. +- Added new functions: ACCRINTM, DISC, INTRATE, PRICEDISC, RECEIVED, YIELDDISC. [#1799](https://github.com/handsontable/hyperformula/pull/1799) ### Fixed From 3ec6b6e27ac6e0d8f5cc9dbda1bed73c9e81b7b7 Mon Sep 17 00:00:00 2001 From: tobiadefami Date: Fri, 9 Oct 2026 21:36:26 +0100 Subject: [PATCH 4/4] Reject negative fractional dates in the securities functions coerceToSecurityDate truncated before checking the range, so a date such as -0.5 became serial 0, a valid date. Excel returns #NUM! for any negative date, before looking at the later arguments. Reject a negative value before truncating, as coerceToDayCountBasis already does for the basis. Co-Authored-By: Claude Opus 5.5 --- src/interpreter/plugin/FinancialPlugin.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/interpreter/plugin/FinancialPlugin.ts b/src/interpreter/plugin/FinancialPlugin.ts index 1a0c28b3dd..a821df544e 100644 --- a/src/interpreter/plugin/FinancialPlugin.ts +++ b/src/interpreter/plugin/FinancialPlugin.ts @@ -1079,13 +1079,16 @@ export class FinancialPlugin extends FunctionPlugin implements FunctionPluginTyp /** * Converts a date argument of a securities function: a number as `strictNumber` accepts it, truncated to an integer. - * A date outside the supported range is #NUM!. + * A date outside the supported range is #NUM!, and so is a negative value even when it truncates to 0, as in Excel. */ private coerceToSecurityDate(value: InternalScalarValue): number | CellError { const dateNumber = this.strictNumber(value) if (dateNumber instanceof CellError) { return dateNumber } + if (dateNumber < 0) { + return new CellError(ErrorType.NUM, ErrorMessage.DateBounds) + } const date = Math.trunc(dateNumber) if (this.dateTimeHelper.getWithinBounds(date) === undefined) { return new CellError(ErrorType.NUM, ErrorMessage.DateBounds)