From 6e8829894a41b91e0afeb2215df226e2e5fe1c0f Mon Sep 17 00:00:00 2001 From: Glenwing Date: Wed, 4 Jul 2018 11:00:09 -0700 Subject: [PATCH] DIU Long Division Update Updated DIU for detection of repeating decimals in aspect ratios Updated bandwidth calculator with preliminary support for same thing --- DebugControl.js | 41 ++++++++ bandwidth/bandwidth.js | 176 +++++++++++++++++++++++++++++++++-- diu/diuJS.js | 207 ++++++++++++++++++++++++++++++++++++++--- diu/index.html | 11 ++- 4 files changed, 408 insertions(+), 27 deletions(-) create mode 100644 DebugControl.js diff --git a/DebugControl.js b/DebugControl.js new file mode 100644 index 0000000..38d4356 --- /dev/null +++ b/DebugControl.js @@ -0,0 +1,41 @@ +DebugSettings = {}; + +function DEBUG(text) { + Fn = DEBUG.caller.name; + proceed_with_msg = false; + + if ('all' in DebugSettings) { + if (!DebugSettings['all']) { + return; + } + else if (DebugSettings['all']) { + proceed_with_msg = true; + } + } + else if (Fn in DebugSettings) { + if (DebugSettings[Fn] == false) { + proceed_with_msg = false; + } + else { + proceed_with_msg = true; + } + } + else { + proceed_with_msg = true; + } + + if (proceed_with_msg == true) { + str = 'console.log("[" + Fn + "()] ::"'; + for (var i = 0; i < arguments.length; i++) { + str += ', arguments[' + i + ']'; + } + str += ');'; + eval(str); + } + + return; +} + +function DebugConfig(settings) { + DebugSettings = settings; +} \ No newline at end of file diff --git a/bandwidth/bandwidth.js b/bandwidth/bandwidth.js index 3a4b8e3..f92d6c4 100644 --- a/bandwidth/bandwidth.js +++ b/bandwidth/bandwidth.js @@ -1697,6 +1697,169 @@ function SameRatio(H, V, A) { } +function long_division(A, B, options) { + if (isNaN(A / B) || !isFinite(A / B)) { DEBUG('Answer is NaN or Infinity. Function aborted.'); return ''; } + + var OL_open = ''; // Overline markup opening tag + var OL_close = ''; // Overline markup closing tag. Used in conjunction with OL_open to surround the repeating numbers. May be set to control markup, separate it with parentheses, or simply left blank, etc. + var p_max = 8; // Maximum number of decimal places + var p_min = 3; // Minimum number of decimal places + var Approx = '≈'; // Symbol to be used for "approximately equal to". Can be set to '~' or blank if desired, etc. + var Radix_Point = '.'; // Character to use for the radix point ("decimal point") + var Base = 10; // Number base system to use + var Minus_Sign = '−'; // Character to preceed negative numbers + var Plus_Sign = ''; // Character to preceed positive numbers + var RepeatSinglesFlag = true; // Display single-digit repeating patterns as 1.(33) instead of 1.(3) + + if (typeof(options) === 'number') { // If 3rd argument is a number rather than a dictionary, use it as the p_max value + p_max = options; + } + else if(options) { + if ('OL_open' in options) { OL_open = options['OL_open']; } + if ('OL_close' in options) { OL_close = options['OL_close']; } + if ('p_max' in options) { p_max = options['p_max']; } + if ('p_min' in options) { p_min = options['p_min']; } + if ('radix' in options) { Radix_Point = options['radix']; } + if ('approx' in options) { Approx = options['approx']; } + if ('minus' in options) { Minus_Sign = options['minus']; } + if ('plus' in options) { Plus_Sign = options['plus']; } + if ('base' in options) { Base = options['base']; } + if ('repeat_singles' in options) { RepeatSinglesFlag = options['repeat_singles']; } + } + + p_max = parseInt(p_max); + p_min = parseInt(p_min); + Base = parseInt(Base); + if (p_max < 0 || p_min < 0 || p_max < p_min || isNaN(p_max) || isNaN(p_min) || !isFinite(p_max) || !isFinite(p_min)) { + DEBUG('Invalid p_max and p_min values. Both values must be non-negative numbers, and p_min cannot be greater than p_max. p_max:', p_max, 'p_min', p_min) + return ''; + } + if (isNaN(Base)) { + DEBUG('Invalid Base value. Must be an integer number. Base:', Base); + return ''; + } + if (p_max == 0) { + var Result = Math.round(A / B).toFixed(0); + if (Result != (A / B)) { Result = '≈'.concat(Result); } + return Result; + } + + var Max_Depth = 32; // Depth of internal calculations, regardless of p_max or p_min settings + var Decimal_Digits = ''; + var Previous_Dividends = {}; + var Prefix = ''; + var Repetend = ''; + var RepeatFlag = false; + var ApproxFlag = true; + var Sign = Plus_Sign; + + // Determine if answer will be negative, then use the absolute value of inputs for the rest of the calculations. + if ((A < 0) ? !(B < 0) : (B < 0)) { Sign = Minus_Sign; } // If (A is negative) XOR (B is negative) then final result will be negative. + var Dividend = Math.abs(parseFloat(A)); + var Divisor = Math.abs(parseFloat(B)); + + var Quotient = Dividend / Divisor; + var Remainder = Dividend % Divisor; + var Result = Math.floor(Quotient).toString() + Radix_Point; // Use floor division to determine the front part of the number immediately + Dividend = Remainder * Base; + + // Use long division for the decimal places, so that repeating decimals can be detected + var i = 0; + while (i < p_max + 2) { + if (!(Dividend in Previous_Dividends)) { + Previous_Dividends[Dividend] = i; + + Quotient = Dividend / Divisor; + Remainder = Dividend % Divisor; + Dividend = Remainder * Base; + + //if (i < p_max) { + Decimal_Digits += Math.floor(Quotient).toString(); + //} + + //DEBUG('i:', i, 'Quotient:', Quotient, 'Remainder:', Remainder, 'Dividend:', Dividend, 'Result:', Result, 'Decimal_Digits:', Decimal_Digits); + } + else { + RepeatFlag = true; + ApproxFlag = false; + Prefix = Decimal_Digits.substring(0, Previous_Dividends[Dividend]); + Repetend = Decimal_Digits.substring(Previous_Dividends[Dividend], Decimal_Digits.length); + + if (Repetend == '0') { // A "repeating" dividend of 0 signals a non-repeating result + Repetend = ''; + RepeatFlag = false; + Decimal_Digits = Prefix; + } + + //Decimal_Digits = Prefix + Repetend; + break; + } + i += 1; + } + + if (RepeatFlag == false) { + if (Decimal_Digits.length > p_max) { + //Decimal_Digits = Decimal_Digits.substr(0, p_max); + Decimal_Digits = Math.round(parseFloat(Decimal_Digits.substr(0, p_max) + '.' + Decimal_Digits.substr(p_max))).toString(); + if (p_max - Decimal_Digits.length >= 0) { + Decimal_Digits = '0'.repeat(p_max - Decimal_Digits.length) + Decimal_Digits; + } + ApproxFlag = true; + } + if (Decimal_Digits.length < p_min) { + if (p_min - Decimal_Digits.length >= 0) { + Decimal_Digits += '0'.repeat(p_min - Decimal_Digits.length); + } + } + } + + if (RepeatFlag == true) { + if (Prefix.length + Repetend.length > p_max) { + if (Prefix.length > p_max) { + Prefix = Math.round(parseFloat(Prefix.substr(0, p_max) + '.' + Prefix.substr(p_max))).toString(); + if (p_max - Prefix.length >= 0) { + Prefix = '0'.repeat(p_max - Prefix.length) + Prefix; + } + } + else { + Prefix = Prefix + Repetend.substr(0, p_max - Prefix.length); + } + Decimal_Digits = Prefix; + RepeatFlag = false; + ApproxFlag = true; + } + if (Prefix.length + Repetend.length < p_min) { + if ((p_min - (Prefix.length + Repetend.length)) >= Repetend.length) { + Repetend += Repetend.repeat(Math.floor((p_min - (Prefix.length + Repetend.length)) / Repetend.length)); + } + while (Prefix.length + Repetend.length < p_min) { + Prefix = Prefix.concat(Repetend[0]); + Repetend = Repetend.slice(1).concat(Repetend[0]); + } + } + } + + if (RepeatFlag == true) { + if (Repetend.length == 1 && (Prefix.length + Repetend.length < p_max) && RepeatSinglesFlag == true) { Repetend = Repetend.repeat(2); } // Single-digit repetitions will be displayed twice, i.e. 4/3 will result in 1.(33) rather than 1.(3) + Result += Prefix + OL_open + Repetend + OL_close; + } + else { + Result += Decimal_Digits; + } + + Result = Sign.concat(Result); + + if (ApproxFlag == true) { + Result = Approx.concat(Result); + } + + if (Result[Result.length - 1] == Radix_Point) { Result = Result.replace(Radix_Point, ''); } + return Result; + + //return Result * Sign; +} + + /* function parseNum(val) { // Converts string to floating point if it has a decimal point, or integer if there is no decimal point. Also strips commas and spaces, and optionally applies absolute value. @@ -1761,15 +1924,14 @@ function parseNum(val) { // First, remove all non-numeric characters on the outsides of the string for (var i = 0; i < val.length; i++) { - if (!(i < val.length)) { break; } - //DEBUG('i:', i, 'val:', val, 'val[i]:', val[i]); // Loop through each character starting from the front + if (!(i < val.length)) { break; } if ((/[^0-9.-]/g).test(val[i]) == true) { // If character is not a number, period, or minus sign, remove it if (i == 0 && val.length > 1) { val = val.slice(1); } else if (i == val.length - 1) { return NaN; } // If this is the last character in the string, then there are no digits in the string; return NaN else if (i > 0) { val = (val.slice(0, i)) + (val.slice(i+1));} - i = i - 1; // Since a character has been removed, the next character is now at the same index + i = i - 1; // Since a character has been removed, the next character is now at the same index, so the loop counter must be adjusted to compensate continue; } else if ((/[-]/g).test(val[i]) == true) { @@ -1777,14 +1939,14 @@ function parseNum(val) { continue; } else if ((/[.]/g).test(val[i]) == true) { - // If character is a period, then following character MUST be a digit, unless it's the end of the number. + // If character is a period, then following character MUST be a digit, unless it's the end of the number. Otherwise the input is not a valid number. if (i + 1 < val.length) { if ((/[0-9]/g).test(val[i+1]) == true) { - // If the character after the period is a digit, then the first digit has been reached + // If the character after the period is a digit, then the "number" part of the number has definitely been reached and the code can proceed with the next section. break; } else { - // If the string contained a period followed by a non-numeric character, it is not a number + // If the string contained a period followed by a non-numeric character, it cannot be interpreted as a valid number. Return NaN. return NaN; } } @@ -1824,7 +1986,7 @@ function parseNum(val) { // Now that the string only contains numeric characters, minus signs, and periods, we must check if there are any invalid usages of the periods and signs, such as multiple periods/signs, or a minus sign anywhere other than the first character. if (val == '' // string is empty - || val.indexOf('.') != val.lastIndexOf('.') // string contains multiple decimal point + || val.indexOf('.') != val.lastIndexOf('.') // string contains multiple periods || val.indexOf('-') != val.lastIndexOf('-') // string contains multiple minus signs || (val.indexOf('-') != -1 && val.indexOf('-') != 0)) // input contains a minus sign and it isn't the first character { diff --git a/diu/diuJS.js b/diu/diuJS.js index 0819bcf..d5a2638 100644 --- a/diu/diuJS.js +++ b/diu/diuJS.js @@ -4,7 +4,11 @@ var unit_select = $('#unit_select input[type="radio"]:checked').val(); var hres1 = parseInt(parseNum($('#INPUT_HRES').val())); var vres1 = parseInt(parseNum($('#INPUT_VRES').val())); - var ar1 = hres1 / vres1; + var ar_options = { + 'p_max': 4, + 'p_min': 2, + } + var ar1 = long_division(hres1, vres1, ar_options); var diag = parseFloat(size); var width = size * Math.sin(Math.atan(hres1 / vres1)); @@ -17,11 +21,11 @@ var vres2 = parseNum($('#INPUT_VRES2').val()); var ar2 = hres2 / vres2; var d_match = Math.sqrt((height * height) * (1 + (ar2 * ar2))); - var opt_res = parseInt(vres1 * ar2) + ' × ' + vres1; + var opt_res = parseInt(vres1 * ar2) + ' × ' + vres1; var hres_den = parseInt(parseNum($('#INPUT_HRES_DENSITY').val())); var vres_den = parseInt(parseNum($('#INPUT_VRES_DENSITY').val())); - var ar_den = hres_den / vres_den; + var ar_den = long_division(hres_den, vres_den, ar_options); var width2 = width * (hres_den / hres1); var height2 = height * (vres_den / vres1); @@ -60,7 +64,8 @@ //if (hres1 != '' && vres1 != '' && hres1 != 0 && vres1 != 0 && isNaN(hres1) == false && isNaN(vres1) == false) { if (isNum([hres1, vres1]) && isPositive([hres1, vres1])) { - $('#RESULT_RATIO').html(commas(ar1.toFixed(3)) + ' (' + parseInt(hres1 / GCD(hres1, vres1)) + ':' + parseInt(vres1 / GCD(hres1, vres1)) + ')'); + // $('#RESULT_RATIO').html(commas(ar1.toFixed(3)) + ' (' + parseInt(hres1 / GCD(hres1, vres1)) + ':' + parseInt(vres1 / GCD(hres1, vres1)) + ')'); + $('#RESULT_RATIO').html(ar1 + '∶1 (' + commas(parseInt(hres1 / GCD(hres1, vres1))) + '∶' + commas(parseInt(vres1 / GCD(hres1, vres1))) + ')'); $('#RESULT_TOTAL_PX').html(commas(total_px) + ' (' + prefixGen(total_px, 2)['num'] + ' ' + prefixGen(total_px, 2)['prefix'] + 'px)'); } else { @@ -71,8 +76,11 @@ //!= '' && vres1 != '' && hres1 != 0 && vres1 != 0 && isNaN(hres1) == false && isNaN(vres1) == false + //DEBUG('isInt', isInt([hres1, vres1, hres_den, vres_den]), 'isPositive', isPositive([hres1, vres1, hres_den, vres_den])); + //DEBUG('hres1', hres1, isInt(hres1), 'vres1', vres1, isInt(vres1), 'hres_den', hres_den, isInt(hres_den), 'vres_den', vres_den, isInt(vres_den)); if (isInt([hres1, vres1, hres_den, vres_den]) && isPositive([hres1, vres1, hres_den, vres_den])) { - $('#RESULT_DENSITY_RATIO').html(commas(ar_den.toFixed(3)) + ' (' + parseInt(hres_den / GCD(hres_den, vres_den)) + ':' + parseInt(vres_den / GCD(hres_den, vres_den)) + ')'); + // $('#RESULT_DENSITY_RATIO').html(commas(ar_den.toFixed(3)) + ' (' + parseInt(hres_den / GCD(hres_den, vres_den)) + ':' + parseInt(vres_den / GCD(hres_den, vres_den)) + ')'); + $('#RESULT_DENSITY_RATIO').html(ar_den + '∶1 (' + commas(parseInt(hres_den / GCD(hres_den, vres_den))) + '∶' + commas(parseInt(vres_den / GCD(hres_den, vres_den))) + ')'); } else { $('#RESULT_DENSITY_RATIO').html(''); @@ -85,32 +93,31 @@ // Returns false if input is a non-integer number or NaN function isInt(num) { - num = parseNum(num); if (Array.isArray(num) == true) { for (a = 0; a < num.length; a++) { - if (Number.isInteger(num[a]) == false) { + if (Number.isInteger(parseNum(num[a])) == false) { + //DEBUG('isInt Array False. a:', a, 'num[a]:', num[a], 'Number.isInteger(num[a]):', Number.isInteger(num[a])); return false; } return true; } } else - return Number.isInteger(num); + return Number.isInteger(parseNum(num)); } function isFloat(num) { - num = parseNum(num); if (Array.isArray(num) == true) { for (a = 0; a < num.length; a++) { - if (Number.isInteger(num[a]) == true || Number.isNaN(num[a]) == true) { + if (Number.isInteger(parseNum(num[a])) == true || Number.isNaN(num[a]) == true) { return false; } return true; } } else - return !(Number.isInteger(num) || Number.isNaN(num)); + return !(Number.isInteger(parseNum(num)) || Number.isNaN(parseNum(num))); } @@ -322,8 +329,178 @@ function GCD(a, b) { } -function commas(x) { - var parts = x.toString().split("."); - parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ","); - return parts.join("."); +function commas(input, group, radix) { + if (!group) { group = ','; } + if (!radix) { radix = '.'; } + var parts = input.toString().split(radix); + parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, group); + return parts.join(radix); +} + + +function long_division(A, B, options) { + if (isNaN(A / B) || !isFinite(A / B)) { DEBUG('Answer is NaN or Infinity. Function aborted.'); return ''; } + + var OL_open = ''; // Overline markup opening tag + var OL_close = ''; // Overline markup closing tag. Used in conjunction with OL_open to surround the repeating numbers. May be set to control markup, separate it with parentheses, or simply left blank, etc. + var p_max = 8; // Maximum number of decimal places + var p_min = 3; // Minimum number of decimal places + var Approx = '≈'; // Symbol to be used for "approximately equal to". Can be set blank to turn off this functionality, etc. + var Radix_Point = '.'; // Character to use for the radix point ("decimal point") + var Group = ','; // Character to use for digit grouping. Can be set blank to disable digit grouping. + var Base = 10; // Number base system to use + var Minus_Sign = '−'; // Character to preceed negative numbers. Default minus sign, can be set blank to output absolute value, or set to hyphen-minus for better compatibility or for other functions to be able to parse as a number correctly. + var Plus_Sign = ''; // Character to preceed positive numbers. Default blank, but can be set to "+" if explicit signs on both positive and negative numbers is desired. + var RepeatSinglesFlag = true; // Display single-digit repeating patterns as 1.(33) instead of 1.(3) + + if (typeof(options) === 'number') { // If 3rd argument is a number rather than a dictionary, use it as the p_max value + p_max = options; + } + else if(options) { + if ('OL_open' in options) { OL_open = options['OL_open']; } + if ('OL_close' in options) { OL_close = options['OL_close']; } + if ('p_max' in options) { p_max = options['p_max']; } + if ('p_min' in options) { p_min = options['p_min']; } + if ('radix' in options) { Radix_Point = options['radix']; } + if ('group' in options) { Group = options['group']; } + if ('approx' in options) { Approx = options['approx']; } + if ('minus' in options) { Minus_Sign = options['minus']; } + if ('plus' in options) { Plus_Sign = options['plus']; } + if ('base' in options) { Base = options['base']; } + if ('repeat_singles' in options) { RepeatSinglesFlag = options['repeat_singles']; } + } + + var Decimal_Digits = ''; + var Previous_Dividends = {}; + var Prefix = ''; + var Repetend = ''; + var RepeatFlag = false; + var ApproxFlag = true; + var Sign = Plus_Sign; + + // Determine if answer will be negative, then use the absolute value of inputs for the rest of the calculations. + if ((A < 0) ? !(B < 0) : (B < 0)) { Sign = Minus_Sign; } // If (A is negative) XOR (B is negative) then final result will be negative. + var Dividend = Math.abs(parseFloat(A)); + var Divisor = Math.abs(parseFloat(B)); + + p_max = parseInt(p_max); + p_min = parseInt(p_min); + Base = parseInt(Base); + if (p_max < 0 || p_min < 0 || p_max < p_min || isNaN(p_max) || isNaN(p_min) || !isFinite(p_max) || !isFinite(p_min)) { + DEBUG('Invalid p_max and p_min values. Both values must be non-negative numbers, and p_min cannot be greater than p_max. p_max:', p_max, 'p_min', p_min) + return ''; + } + if (!isInt(Base)) { + DEBUG('Invalid Base value. Must be an integer number. Base:', Base); + return ''; + } + if (p_max == 0) { // If p_max is 0, then output is integer values only, and the long division section is not necessary. + var Result = Math.round(Dividend / Divisor).toFixed(0); + if (Result != (Dividend / Divisor)) { ApproxFlag = true; } + Result = Sign.concat(Result.toString()); + if (ApproxFlag == true) { Result = Approx.concat(Result); } + return Result; + } + + var Quotient = Dividend / Divisor; + var Remainder = Dividend % Divisor; + var Result = Math.floor(Quotient).toString() + Radix_Point; // Use floor division to determine the front part of the number immediately + Dividend = Remainder * Base; + + // Use long division for the decimal places, so that repeating decimals can be detected + var i = 0; + while (i < p_max + 2) { + if (!(Dividend in Previous_Dividends)) { + Previous_Dividends[Dividend] = i; + + Quotient = Dividend / Divisor; + Remainder = Dividend % Divisor; + Dividend = Remainder * Base; + + //if (i < p_max) { + Decimal_Digits += Math.floor(Quotient).toString(); + //} + + //DEBUG('i:', i, 'Quotient:', Quotient, 'Remainder:', Remainder, 'Dividend:', Dividend, 'Result:', Result, 'Decimal_Digits:', Decimal_Digits); + } + else { + RepeatFlag = true; + ApproxFlag = false; + Prefix = Decimal_Digits.substring(0, Previous_Dividends[Dividend]); + Repetend = Decimal_Digits.substring(Previous_Dividends[Dividend], Decimal_Digits.length); + + if (Repetend == '0') { // A "repeating" dividend of 0 signals a non-repeating result + Repetend = ''; + RepeatFlag = false; + Decimal_Digits = Prefix; + } + + //Decimal_Digits = Prefix + Repetend; + break; + } + i += 1; + } + + if (RepeatFlag == false) { + if (Decimal_Digits.length > p_max) { + //Decimal_Digits = Decimal_Digits.substr(0, p_max); + Decimal_Digits = Math.round(parseFloat(Decimal_Digits.substr(0, p_max) + '.' + Decimal_Digits.substr(p_max))).toString(); + if (p_max - Decimal_Digits.length >= 0) { + Decimal_Digits = '0'.repeat(p_max - Decimal_Digits.length) + Decimal_Digits; + } + ApproxFlag = true; + } + if ((Decimal_Digits.length < p_min) && (p_min - Decimal_Digits.length >= 0)) { + Decimal_Digits += '0'.repeat(p_min - Decimal_Digits.length); + } + } + + if (RepeatFlag == true) { + if (Prefix.length + Repetend.length > p_max) { + if (Prefix.length > p_max) { + Prefix = Math.round(parseFloat(Prefix.substr(0, p_max) + '.' + Prefix.substr(p_max))).toString(); + if (p_max - Prefix.length >= 0) { + Prefix = '0'.repeat(p_max - Prefix.length) + Prefix; + } + } + else { + Prefix = Prefix + Repetend.substr(0, p_max - Prefix.length); + } + Decimal_Digits = Prefix; + RepeatFlag = false; + ApproxFlag = true; + } + if (Prefix.length + Repetend.length < p_min) { + if ((p_min - (Prefix.length + Repetend.length)) >= Repetend.length) { + Repetend += Repetend.repeat(Math.floor((p_min - (Prefix.length + Repetend.length)) / Repetend.length)); + } + while (Prefix.length + Repetend.length < p_min) { + Prefix = Prefix.concat(Repetend[0]); + Repetend = Repetend.slice(1).concat(Repetend[0]); + } + } + } + + if (RepeatFlag == true) { + if (Repetend.length == 1 && (Prefix.length + Repetend.length < p_max) && RepeatSinglesFlag == true) { Repetend = Repetend.repeat(2); } // Single-digit repetitions will be displayed twice, i.e. 4/3 will result in 1.(33) rather than 1.(3) + Result += Prefix + OL_open + Repetend + OL_close; + } + else { + Result += Decimal_Digits; + } + + if (Result[Result.length - 1] == Radix_Point) { Result = Result.replace(Radix_Point, ''); } + + + Result = commas(Result, Group, Radix_Point); + + Result = Sign.concat(Result); + + if (ApproxFlag == true) { + Result = Approx.concat(Result); + } + + return Result; + + //return Result * Sign; } \ No newline at end of file diff --git a/diu/index.html b/diu/index.html index 5f166e4..d048b54 100644 --- a/diu/index.html +++ b/diu/index.html @@ -3,6 +3,7 @@ + @@ -85,17 +86,17 @@ Secondary Display
Aspect Ratio: - : +   - Warning: Formats commonly known as "21:9" are not an exact 21:9 ratio
-   2560 1080 is a 64:27 ratio
-   3440 1440 is a 43:18 ratio
-   3840 1600 is a 12:5 ratio
+ Warning: Formats commonly known as "21∶9" are not an exact 21∶9 ratio
+   2560 1080 is a 64∶27 ratio
+   3440 1440 is a 43∶18 ratio
+   3840 1600 is a 12∶5 ratio