In financial products, the term tenor has multiple usages but it is commonly it is referred to as the time remaining for a loan repayment or a financial contract matures/expires.
In a recent exercise I had the requirement to sort some financial data content by tenor, but the tenor itself was hand-entered, the frequency or duration used a number of different units, and it was represented as a string, eg. 1y6m represented 18 months, 19m represented 19 months, 1w2d and 9d both represented 9 days, 11d represented 11 days, and 3m 2y2w1d was short hand for 2 years 3 months 2 weeks and 1 day. Clearly sorting the shorthand tenor string alphabetically would resulted in the ordered (ascending) list of:
11d 19m 1w2d 1y6m 3m 2y2w1d 9d
This is clearly not a useful ordering for most purposes and it is certainly not tenor ordering from from lowest to highest in a numeric sense. If the tenor were normalised to “Days” and then ordered numerically, the expected tenor could be manipulated in a more useful manner, including some mathematical operations. In this case ordering (again ascending) would result in:
9 // 9d 9 // 1w2d = 7 + 2 11 // 11d 545 // 1y6m = 365 + 6*30 570 // 19m = 19*30 days/month = 570 835 // 3m 2Y2w1d = 3*60 + 2*365 + 2*7 + 1
The issue I had is how, in C#, could the irregularly formatted/hand-entered tenor (so it may contain errors, such as “1d2d”, or where it was endowed with additional white-space as in “3m 2Y2w1d” or ordering of mywd rather than ymwd that would more be the norm or mixtures of upper and lower case or …..) be converted to Days for the purpose of ordering?
A code monkey may well split the tenor string into a char array, write some sort of loop-within-loops code construct with indexing of the char position (with careful attention to the array [0] and [length] edge cases), and more than a few variables with names similar to i and j. Getting this type of logic correct will likely involve trial and error too. Programmers of this calibre rarely write unit tests, and they also comment their code with comments like “counter variable for character position”, so the source code produced will not be maintainable from a number of perspectives. To labour the point further, the code will look similar to the code that hackers produce, or a 16 year old writes in the bedroom after bunking off school for the afternoon, or that is pulled off StackOverflow and copy/pasted verbatim into the codebase. I need not paint the picture further!
I try not to produce spaghetti code and when the option is available to me, and try to write code using the right technologies too. To this end, my mindset when approaching this type of problem from a greenfield perspective is to use both LINQ and Regular Expressions, technologies that lend themselves to the problem at hand.
I show my example code below, where I normalise the Tenor to Days (there are of course assumptions, each year has 365 days, a month 30, and so on) for ordering.
using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; namespace uk.co.woodwardinformatics.tenor { /// <summary> /// <para> /// Tenor /// </para> /// </summary> public class Tenor { //tenor bucket size, defined hard-coded values as unlikely to change, ConfigurationManager.AppSettings perhaps? public const String DELIMITERDAY = "d"; //notation suffix: d=days, w=weeks, ... public const String DELIMITERWEEK = "w"; public const String DELIMITERMONTH = "m"; public const String DELIMITERYEAR = "y"; public static readonly Dictionary<String, int> tenorBucket = new Dictionary<String, int> { { DELIMITERDAY, 1 }, //as above, 1, 7 etc. ConfigurationManager.AppSettings perhaps? { DELIMITERWEEK, 7 }, { DELIMITERMONTH, 30 }, { DELIMITERYEAR, 365 } }; private Tenor() { } public Tenor(String tenor) : this() { Representation = tenor; Days = -1; try { var rx = new Regex($"({DELIMITERDAY}|{DELIMITERWEEK}|{DELIMITERMONTH}|{DELIMITERYEAR})") //lex out as pairs the tenor interval (d,m,w,y) and duration 3 (as in 3d) .Split(tenor.ToLower().Trim()) .Where(s => !String.IsNullOrEmpty(s)) .Select(s => s); if (0 == rx.GroupBy(s => s) //quick check to ensure that each tenor interval is specified at most once, eg. at most one d, at most one m, and so on .Where(g => g.Count() > 1 && g.Key.All(Char.IsLetter)) .Count() ) //Normalised the tenor to approximate days (from days, weeks, months, and years) for ease of sorting Days = rx.Select((val, index) => new { Index = index, Value = val }) .GroupBy(g => g.Index / 2) .Select(g => new { Days = int.Parse(g.ElementAt(0).Value) * tenorBucket[g.ElementAt(1).Value] }) .Sum(g => g.Days); } catch { } finally { IsValid = -1 != Days; } } public static implicit operator Tenor(String tenor) { return new Tenor(tenor); } public static implicit operator String(Tenor tenor) { return tenor.Representation; } /// <summary> /// <para> /// If the tenor period 'string' is valid, returns <i>true</i> if valid, <i>false</i> otherwise /// </para> /// </summary> public Boolean IsValid { get; private set; } /// <summary> /// <para> /// The unadulterated representation of the tenor period /// </para> /// </summary> public String Representation { get; private set; } /// <summary> /// <para> /// An approximation (years aren't exactly 365 days, months aren't exactly 30 days, ..., but these are the values used for tenor /// day calculations) of the number of days in the tenor /// </para> /// </summary> public int Days { get; private set; } } }
A word or two of discussion is required here. The code above is a simple C# class that does one thing – representing a tenor exposing the duration or frequency of the tenor as a number (through property Days) that, as Days is a numeric data type, can be used for maths and ordering etc.
The key logic within this class is encapsulated by the LINQ and Regular Expressions (and although trivial, it did take me longer than I would have liked to write) and the source code and its purpose are very clear!
Lastly, if you can’t be bothered to test your code, you shouldn’t bother writing it, so how did I test my code? The answer, and the answer every developer will give, at least, is by debugging and running it, probably in Visual Studio. Of course in large systems, a more regimented unit test suite would be required.
I wrote, in part, the following tests/test code.
using Microsoft.VisualStudio.TestTools.UnitTesting; namespace uk.co.woodwardinformatics.tests.tenor { /// <summary> /// <para> /// Test class for Tenor /// </para> /// <see cref="uk.co.woodwardinformatics.tenor.Tenor"/> /// </summary> [TestClass] public class TenorUnitTests { #region Implicit Conversion [TestMethod] public void TenorImplicitConversion() { Tenor tenor = "2d3w1m5y"; Assert.AreEqual(2+(3*7)+30+(5*365), tenor.Days); Assert.IsTrue(tenor.IsValid); } #endregion #region Days calculation [TestMethod] public void Tenor2d() { Tenor tenor = new Tenor("2d"); Assert.AreEqual(2, tenor.Days); Assert.IsTrue(tenor.IsValid); } [TestMethod] public void Tenor1w() { Tenor tenor = new Tenor("1w"); Assert.AreEqual(7, tenor.Days); Assert.IsTrue(tenor.IsValid); } [TestMethod] public void Tenor1m() { Tenor tenor = new Tenor("1m"); Assert.AreEqual(30, tenor.Days); Assert.IsTrue(tenor.IsValid); } [TestMethod] public void Tenor2m() { Tenor tenor = new Tenor("2m"); Assert.AreEqual(2*30, tenor.Days); Assert.IsTrue(tenor.IsValid); } [TestMethod] public void Tenor0y2d() { Tenor tenor = new Tenor("0y2d0m"); Assert.AreEqual(2, tenor.Days); Assert.IsTrue(tenor.IsValid); } [TestMethod] public void Tenor2y() { Tenor tenor = new Tenor("2y"); Assert.AreEqual(365*2, tenor.Days); Assert.IsTrue(tenor.IsValid); } [TestMethod] public void Tenor2() { Tenor tenor = new Tenor("2"); Assert.IsFalse(tenor.IsValid); } [TestMethod] public void Tenor1d1w() { Tenor tenor = new Tenor("1d1w"); Assert.AreEqual(1 + 7, tenor.Days); Assert.IsTrue(tenor.IsValid); } [TestMethod] public void Tenor2d3w4m1y() { Tenor tenor = new Tenor("2d3w4m1y"); Assert.AreEqual(2 + (3 * 7) + (4 * 30) + 365, tenor.Days); Assert.IsTrue(tenor.IsValid); } [TestMethod] public void Tenor4m1y2d3w() { Tenor tenor = new Tenor("4m1y2d3w"); Assert.AreEqual((4 * 30) + 365 + 2 + (3 * 7), tenor.Days); Assert.IsTrue(tenor.IsValid); } [TestMethod] public void Tenor__4m_1y2d3w___() { Tenor tenor = new Tenor(" 4m 1y2d3w "); //spaces, still a very valid tenor Assert.AreEqual((4 * 30) + 365 + 2 + (3 * 7), tenor.Days); Assert.IsTrue(tenor.IsValid); } [TestMethod] public void TenorCaseInsensitive4m1Y2D3w() { Tenor tenor = new Tenor("4m1Y2D3w"); Assert.AreEqual((4 * 30) + 365 + 2 + (3 * 7), tenor.Days); Assert.IsTrue(tenor.IsValid); } #endregion [TestMethod] public void TenorInvalid1dd() { Tenor tenor = new Tenor("1dd"); Assert.IsFalse(tenor.IsValid); } [TestMethod] public void TenorInvalid1d2d() { Tenor tenor = new Tenor("1d2d"); Assert.IsFalse(tenor.IsValid); } #region Representation [TestMethod] public void TenorRepresentationValid() { Tenor tenor = new Tenor("2d"); Assert.AreEqual("2d", tenor.Representation); } [TestMethod] public void TenorRepresentationInvalid() { Tenor tenor = new Tenor("foo"); Assert.AreEqual("foo", tenor.Representation); } #endregion } }
In conclusion, the source code demonstrates much, but importantly:
— Published by Mike, 12:33 09 April 2017
By Month: November 2022, October 2022, August 2022, February 2021, January 2021, December 2020, November 2020, March 2019, September 2018, June 2018, May 2018, April 2018
Apple, C#, Databases, Faircom, General IT Rant, German, Informatics, LINQ, MongoDB, Oracle, Perl, PostgreSQL, SQL, SQL Server, Unit Testing, XML/XSLT
Leave a Reply