ixbrl-viewer 1.4.13__py3-none-any.whl → 1.4.15__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of ixbrl-viewer might be problematic. Click here for more details.

Files changed (44) hide show
  1. iXBRLViewerPlugin/__init__.py +27 -26
  2. iXBRLViewerPlugin/_version.py +2 -2
  3. iXBRLViewerPlugin/constants.py +2 -1
  4. iXBRLViewerPlugin/iXBRLViewer.py +17 -8
  5. iXBRLViewerPlugin/ui.py +5 -4
  6. iXBRLViewerPlugin/viewer/dist/ixbrlviewer.js +1 -1
  7. iXBRLViewerPlugin/viewer/src/html/inspector.html +27 -0
  8. iXBRLViewerPlugin/viewer/src/i18n/en/translation.json +18 -1
  9. iXBRLViewerPlugin/viewer/src/i18n/es/translation.json +18 -1
  10. iXBRLViewerPlugin/viewer/src/icons/calculator.svg +13 -0
  11. iXBRLViewerPlugin/viewer/src/icons/circle-cross.svg +11 -0
  12. iXBRLViewerPlugin/viewer/src/icons/circle-tick.svg +11 -0
  13. iXBRLViewerPlugin/viewer/src/icons/dimension.svg +1 -5
  14. iXBRLViewerPlugin/viewer/src/icons/member.svg +2 -5
  15. iXBRLViewerPlugin/viewer/src/icons/multi-tag.svg +10 -0
  16. iXBRLViewerPlugin/viewer/src/js/accordian.js +2 -2
  17. iXBRLViewerPlugin/viewer/src/js/calculation.js +202 -0
  18. iXBRLViewerPlugin/viewer/src/js/calculation.test.js +306 -0
  19. iXBRLViewerPlugin/viewer/src/js/calculationInspector.js +190 -0
  20. iXBRLViewerPlugin/viewer/src/js/concept.js +7 -1
  21. iXBRLViewerPlugin/viewer/src/js/fact.js +59 -5
  22. iXBRLViewerPlugin/viewer/src/js/factset.js +54 -5
  23. iXBRLViewerPlugin/viewer/src/js/factset.test.js +71 -5
  24. iXBRLViewerPlugin/viewer/src/js/inspector.js +85 -30
  25. iXBRLViewerPlugin/viewer/src/js/interval.js +70 -0
  26. iXBRLViewerPlugin/viewer/src/js/interval.test.js +153 -0
  27. iXBRLViewerPlugin/viewer/src/js/test-utils.js +19 -0
  28. iXBRLViewerPlugin/viewer/src/js/viewer.js +6 -8
  29. iXBRLViewerPlugin/viewer/src/less/accordian.less +2 -2
  30. iXBRLViewerPlugin/viewer/src/less/calculation-inspector.less +83 -0
  31. iXBRLViewerPlugin/viewer/src/less/colours.less +2 -0
  32. iXBRLViewerPlugin/viewer/src/less/common.less +3 -3
  33. iXBRLViewerPlugin/viewer/src/less/dialog.less +8 -4
  34. iXBRLViewerPlugin/viewer/src/less/inspector.less +69 -25
  35. iXBRLViewerPlugin/viewer/src/less/validation-report.less +1 -2
  36. iXBRLViewerPlugin/viewer/src/less/viewer.less +12 -12
  37. {ixbrl_viewer-1.4.13.dist-info → ixbrl_viewer-1.4.15.dist-info}/METADATA +3 -3
  38. {ixbrl_viewer-1.4.13.dist-info → ixbrl_viewer-1.4.15.dist-info}/RECORD +43 -34
  39. iXBRLViewerPlugin/viewer/src/js/calculations.js +0 -111
  40. {ixbrl_viewer-1.4.13.dist-info → ixbrl_viewer-1.4.15.dist-info}/LICENSE +0 -0
  41. {ixbrl_viewer-1.4.13.dist-info → ixbrl_viewer-1.4.15.dist-info}/NOTICE +0 -0
  42. {ixbrl_viewer-1.4.13.dist-info → ixbrl_viewer-1.4.15.dist-info}/WHEEL +0 -0
  43. {ixbrl_viewer-1.4.13.dist-info → ixbrl_viewer-1.4.15.dist-info}/entry_points.txt +0 -0
  44. {ixbrl_viewer-1.4.13.dist-info → ixbrl_viewer-1.4.15.dist-info}/top_level.txt +0 -0
@@ -2,16 +2,29 @@
2
2
 
3
3
  import { Fact } from "./fact.js";
4
4
  import { Footnote } from "./footnote.js";
5
+ import { Interval } from './interval.js';
5
6
 
6
7
  export class FactSet {
7
- constructor(items) {
8
- this._items = items;
8
+
9
+ constructor (items) {
10
+ this.itemMap = new Map();
11
+ for (const item of items ?? []) {
12
+ this.add(item)
13
+ }
14
+ }
15
+
16
+ add(item) {
17
+ this.itemMap.set(item.vuid, item);
18
+ }
19
+
20
+ items() {
21
+ return Array.from(this.itemMap.values());
9
22
  }
10
23
 
11
24
  /* Returns the union of dimensions present on facts in the set */
12
25
  _allDimensions() {
13
26
  const dims = {};
14
- const facts = this._items.filter((item) => item instanceof Fact);
27
+ const facts = this.items().filter((item) => item instanceof Fact);
15
28
  for (const fact of facts) {
16
29
  const dd = Object.keys(fact.dimensions());
17
30
  for (var j = 0; j < dd.length; j++) {
@@ -30,7 +43,7 @@ export class FactSet {
30
43
  */
31
44
  minimallyUniqueLabel(fact) {
32
45
  if (!this._minimallyUniqueLabels) {
33
- var facts = this._items.filter((item) => item instanceof Fact);
46
+ var facts = this.items().filter((item) => item instanceof Fact);
34
47
  var allLabels = {};
35
48
  var allAspects = ["c", "p"].concat(this._allDimensions());
36
49
  /* Assemble a map of arrays of all aspect labels for all facts, in a
@@ -86,7 +99,7 @@ export class FactSet {
86
99
  }
87
100
  }
88
101
 
89
- this._items.filter((item) => item instanceof Footnote).forEach((fn) => {
102
+ this.items().filter((item) => item instanceof Footnote).forEach((fn) => {
90
103
  uniqueLabels[fn.vuid] = [fn.title];
91
104
  });
92
105
 
@@ -94,4 +107,40 @@ export class FactSet {
94
107
  }
95
108
  return this._minimallyUniqueLabels[fact.vuid].join(", ");
96
109
  }
110
+
111
+ isEmpty() {
112
+ return this.itemMap.size == 0;
113
+ }
114
+
115
+ /*
116
+ * Returns an Interval for the intersection of all values in the set, or
117
+ * undefined if there is no intersection (inconsistent duplicates)
118
+ */
119
+ valueIntersection() {
120
+ const duplicates = this.items().map(fact => Interval.fromFact(fact));
121
+ return Interval.intersection(...duplicates);
122
+ }
123
+
124
+ completeDuplicates() {
125
+ return this.items().every(f => f.isCompleteDuplicate(this.items()[0]));
126
+ }
127
+
128
+ isConsistent() {
129
+ if (this.itemMap.size == 0) {
130
+ return true;
131
+ }
132
+ const duplicates = this.items().map(fact => Interval.fromFact(fact));
133
+ return Interval.intersection(...duplicates) !== undefined;
134
+ }
135
+
136
+ size() {
137
+ return this.itemMap.size;
138
+ }
139
+
140
+ /*
141
+ * Return the most precise (highest decimals) value within the set.
142
+ */
143
+ mostPrecise() {
144
+ return this.items().reduce((a, b) => b.isMorePrecise(a) ? b : a);
145
+ }
97
146
  }
@@ -1,8 +1,10 @@
1
1
  // See COPYRIGHT.md for copyright information
2
2
 
3
3
  import { FactSet } from "./factset.js";
4
+ import { Fact } from "./fact.js";
4
5
  import { NAMESPACE_ISO4217, viewerUniqueId } from "./util.js";
5
6
  import { ReportSet } from "./reportset.js";
7
+ import './test-utils.js';
6
8
 
7
9
  var i = 0;
8
10
 
@@ -71,7 +73,7 @@ var testReportData = {
71
73
  }
72
74
  };
73
75
 
74
- function testReport(facts) {
76
+ function testReportSet(facts) {
75
77
  // Deep copy of standing data
76
78
  const data = JSON.parse(JSON.stringify(testReportData));
77
79
  data.facts = facts;
@@ -90,7 +92,7 @@ function getFact(reportSet, id) {
90
92
  }
91
93
 
92
94
  describe("Minimally unique labels (non-dimensional)", () => {
93
- const reportSet = testReport({
95
+ const reportSet = testReportSet({
94
96
  "f1": testFact({"c": "eg:Concept1", "p": "2018-01-01"}),
95
97
  "f2": testFact({"c": "eg:Concept2", "p": "2018-01-01"}),
96
98
  "f3": testFact({"c": "eg:Concept2", "p": "2019-01-01"}),
@@ -133,7 +135,7 @@ describe("Minimally unique labels (non-dimensional)", () => {
133
135
  });
134
136
 
135
137
  describe("Minimally unique labels (dimensional)", () => {
136
- const reportSet = testReport({
138
+ const reportSet = testReportSet({
137
139
  "f1": testFact({"c": "eg:Concept1", "p": "2018-01-01", "eg:Dimension1": "eg:DimensionValue1"}),
138
140
  "f2": testFact({"c": "eg:Concept1", "p": "2018-01-01", "eg:Dimension1": "eg:DimensionValue2"}),
139
141
  "f3": testFact({"c": "eg:Concept1", "p": "2019-01-01", "eg:Dimension1": "eg:DimensionValue2"}),
@@ -172,7 +174,7 @@ describe("Minimally unique labels (dimensional)", () => {
172
174
  });
173
175
 
174
176
  describe("Minimally unique labels (duplicate facts)", () => {
175
- const reportSet = testReport({
177
+ const reportSet = testReportSet({
176
178
  "f1": testFact({"c": "eg:Concept1", "p": "2018-01-01", "eg:Dimension1": "eg:DimensionValue1"}),
177
179
  "f2": testFact({"c": "eg:Concept1", "p": "2018-01-01", "eg:Dimension1": "eg:DimensionValue1"}),
178
180
  });
@@ -189,7 +191,7 @@ describe("Minimally unique labels (duplicate facts)", () => {
189
191
  });
190
192
 
191
193
  describe("Minimally unique labels (missing labels)", () => {
192
- const reportSet = testReport({
194
+ const reportSet = testReportSet({
193
195
  "f1": testFact({"c": "eg:Concept1", "p": "2018-01-01" }),
194
196
  "f2": testFact({"c": "eg:Concept4", "p": "2018-01-01" }),
195
197
  });
@@ -204,3 +206,67 @@ describe("Minimally unique labels (missing labels)", () => {
204
206
  expect(fs.minimallyUniqueLabel(f2)).toEqual("eg:Concept4");
205
207
  });
206
208
  });
209
+
210
+ function numericTestFact(value, decimals) {
211
+ var factData = { "d": decimals, "v": value, "a": { "c": "eg:Concept1", "u": "eg:pure" }};
212
+ return factData;
213
+ }
214
+
215
+ describe("Consistency", () => {
216
+ const reportSet = testReportSet({
217
+ "f1": numericTestFact(150, -1), // 145-155
218
+ "f2": numericTestFact(200, -2), // 150-250
219
+ "f3": numericTestFact(140, -1), // 135-145
220
+ });
221
+
222
+ const f1 = getFact(reportSet, "f1");
223
+ const f2 = getFact(reportSet, "f2");
224
+ const f3 = getFact(reportSet, "f3");
225
+
226
+ test("Inconsistent fact set", () => {
227
+ const factSet = new FactSet([f1, f2, f3]);
228
+ expect(factSet.valueIntersection()).toBeUndefined();
229
+ expect(factSet.isConsistent()).toBeFalsy();
230
+ });
231
+
232
+ test("Consistent fact sets - overlap", () => {
233
+ const factSet = new FactSet([f1, f2]);
234
+ const intersection = factSet.valueIntersection();
235
+ expect(intersection.a).toEqualDecimal(150);
236
+ expect(intersection.b).toEqualDecimal(155);
237
+ expect(factSet.isConsistent()).toBeTruthy();
238
+ });
239
+
240
+ test("Consistent fact sets - bounds coincide", () => {
241
+ const factSet = new FactSet([f1, f3]);
242
+ const intersection = factSet.valueIntersection();
243
+ expect(intersection.a).toEqualDecimal(145);
244
+ expect(intersection.b).toEqualDecimal(145);
245
+ expect(factSet.isConsistent()).toBeTruthy();
246
+ });
247
+
248
+ });
249
+
250
+ describe("Most precise", () => {
251
+ const reportSet = testReportSet({
252
+ "f1": numericTestFact(150, -1),
253
+ "f2": numericTestFact(200, 0),
254
+ "f3": numericTestFact(140, 2),
255
+ "f4": numericTestFact(160, undefined)
256
+ });
257
+
258
+ const f1 = getFact(reportSet, "f1");
259
+ const f2 = getFact(reportSet, "f2");
260
+ const f3 = getFact(reportSet, "f3");
261
+ const f4 = getFact(reportSet, "f4");
262
+
263
+ test("Most precise - finite decimals", () => {
264
+ const factSet = new FactSet([f1, f2, f3]);
265
+ expect(factSet.mostPrecise()).toEqual(f3);
266
+ });
267
+
268
+ test("Most precise - mixture of finite and infinite", () => {
269
+ const factSet = new FactSet([f1, f2, f3, f4]);
270
+ expect(factSet.mostPrecise()).toEqual(f4);
271
+ });
272
+ });
@@ -1,12 +1,11 @@
1
1
  // See COPYRIGHT.md for copyright information
2
2
 
3
3
  import $ from 'jquery'
4
+ import i18next from 'i18next';
5
+ import jqueryI18next from 'jquery-i18next';
4
6
  import { formatNumber, wrapLabel, truncateLabel, runGenerator, SHOW_FACT, HIGHLIGHT_COLORS, viewerUniqueId } from "./util.js";
5
7
  import { ReportSearch } from "./search.js";
6
- import { Calculation } from "./calculations.js";
7
8
  import { IXBRLChart } from './chart.js';
8
- import i18next from 'i18next';
9
- import jqueryI18next from 'jquery-i18next';
10
9
  import { ViewerOptions } from './viewerOptions.js';
11
10
  import { Identifiers } from './identifiers.js';
12
11
  import { Menu } from './menu.js';
@@ -17,6 +16,9 @@ import { Footnote } from './footnote.js';
17
16
  import { ValidationReportDialog } from './validationreport.js';
18
17
  import { TextBlockViewerDialog } from './textblockviewer.js';
19
18
  import { MessageBox } from './messagebox.js';
19
+ import { Interval } from './interval.js';
20
+ import { Calculation } from "./calculation.js";
21
+ import { CalculationInspector } from './calculationInspector.js';
20
22
  import { ReportSetOutline } from './outline.js';
21
23
  import { DIMENSIONS_KEY, DocumentSummary, MEMBERS_KEY, PRIMARY_ITEMS_KEY, TOTAL_KEY } from './summary.js';
22
24
 
@@ -27,6 +29,7 @@ export class Inspector {
27
29
  this._iv = iv;
28
30
  this._viewerOptions = new ViewerOptions()
29
31
  this._currentItem = null;
32
+ this._useCalc11 = true;
30
33
  }
31
34
 
32
35
  i18nInit() {
@@ -215,6 +218,7 @@ export class Inspector {
215
218
  const iv = this._iv;
216
219
  this._toolbarMenu.reset();
217
220
  this._toolbarMenu.addCheckboxItem(i18next.t("toolbar.xbrlElements"), (checked) => this.highlightAllTags(checked), "highlight-tags", null, this._iv.options.highlightTagsOnStartup);
221
+ this._toolbarMenu.addCheckboxItem(i18next.t("calculation.calculations11"), (useCalc11) => this.setCalculationMode(useCalc11), "calculation-mode", "select-language", this._useCalc11);
218
222
  if (iv.isReviewModeEnabled()) {
219
223
  this._toolbarMenu.addCheckboxItem("Untagged Numbers", function (checked) {
220
224
  const body = iv.viewer.contents().find("body");
@@ -262,6 +266,13 @@ export class Inspector {
262
266
  }
263
267
  }
264
268
 
269
+ setCalculationMode(useCalc11) {
270
+ this._useCalc11 = useCalc11;
271
+ if (this._currentItem instanceof Fact) {
272
+ this.updateCalculation(this._currentItem);
273
+ }
274
+ }
275
+
265
276
  highlightAllTags(checked) {
266
277
  this._viewer.highlightAllTags(checked, this._reportSet.namespaceGroups());
267
278
  }
@@ -464,7 +475,7 @@ export class Inspector {
464
475
  }
465
476
 
466
477
  updateCalculation(fact, elr) {
467
- $('.calculations .tree').empty().append(this._calculationHTML(fact, elr));
478
+ $('.calculations .tree').empty().append(this._calculationHTML(fact));
468
479
  }
469
480
 
470
481
  createSummary() {
@@ -682,47 +693,91 @@ export class Inspector {
682
693
  }
683
694
 
684
695
  _calculationHTML(fact, elr) {
685
- const calc = new Calculation(fact);
696
+ const calc = new Calculation(fact, this._useCalc11);
686
697
  if (!calc.hasCalculations()) {
687
698
  return "";
688
699
  }
689
700
  const tableFacts = this._viewer.factsInSameTable(fact);
690
- if (!elr) {
691
- elr = calc.bestELRForFactSet(tableFacts);
692
- }
701
+ const selectedELR = calc.bestELRForFactSet(tableFacts);
693
702
  const report = fact.report;
694
703
  const inspector = this;
695
704
  const a = new Accordian();
696
705
 
697
- for (const [e, rolePrefix] of Object.entries(calc.elrs())) {
698
- const label = report.getRoleLabel(rolePrefix);
699
-
700
- const rCalc = calc.resolvedCalculation(e);
706
+ for (const rCalc of calc.resolvedCalculations()) {
707
+ const label = report.getRoleLabel(rCalc.elr);
701
708
  const calcBody = $('<div></div>');
702
- for (const [i, r] of rCalc.entries()) {
703
- const itemHTML = $("<div></div>")
709
+ const calcTable = $('<table></table>')
710
+ .addClass("calculation-table")
711
+ .appendTo(calcBody);
712
+
713
+ for (const r of rCalc.rows) {
714
+ const itemHTML = $("<tr></tr>")
704
715
  .addClass("item")
705
- .append($("<span></span>").addClass("weight").text(r.weightSign + " "))
706
- .append($("<span></span>").addClass("concept-name").text(report.getLabelOrName(r.concept, "std")))
707
- .appendTo(calcBody);
716
+ .append($("<td></td>").addClass("weight").text(r.weightSign + " "))
717
+ .append($("<td></td>").addClass("concept-name").text(r.concept.label()))
718
+ .append($("<td></td>").addClass("value"))
719
+ .appendTo(calcTable);
708
720
 
709
- // r.facts is a map of fact IDs to Fact objects
710
- if (r.facts) {
721
+ if (!r.facts.isEmpty()) {
722
+ itemHTML.addClass("calc-fact-link");
711
723
  itemHTML.addClass("calc-fact-link");
712
- itemHTML.data('ivids', Object.keys(r.facts));
713
- itemHTML.click(() => inspector.selectItem(Object.values(r.facts)[0].vuid));
714
- itemHTML.mouseenter(() => Object.values(r.facts).forEach(f => this._viewer.linkedHighlightFact(f)));
715
- itemHTML.mouseleave(() => Object.values(r.facts).forEach(f => this._viewer.clearLinkedHighlightFact(f)));
716
- Object.values(r.facts).forEach(f => this._viewer.highlightRelatedFact(f));
724
+ itemHTML.data('ivids', r.facts.items().map(f => f.vuid));
725
+ itemHTML.click(() => this.selectItem(r.facts.items[0].vuid));
726
+ itemHTML.mouseenter(() => r.facts.items().forEach(f => this._viewer.linkedHighlightFact(f)));
727
+ itemHTML.mouseleave(() => r.facts.items().forEach(f => this._viewer.clearLinkedHighlightFact(f)));
728
+ r.facts.items().forEach(f => this._viewer.highlightRelatedFact(f));
729
+ itemHTML.find(".value").text(r.facts.mostPrecise().readableValue());
717
730
  }
718
731
  }
719
- $("<div></div>").addClass("item").addClass("total")
720
- .append($("<span></span>").addClass("weight"))
721
- .append($("<span></span>").addClass("concept-name").text(fact.getLabelOrName("std")))
732
+ $("<tr></tr>").addClass("item").addClass("total")
733
+ .append($("<td></td>").addClass("weight"))
734
+ .append($("<td></td>").addClass("concept-name").text(fact.concept().label()))
735
+ .append($("<td></td>").addClass("value").text(fact.readableValue()))
736
+ .appendTo(calcTable);
737
+
738
+ const calcStatusIcon = $("<span></span>");
739
+ const cardTitle = $("<span></span>")
740
+ .append(calcStatusIcon)
741
+ .append($("<span></span>").text(label));
742
+ const calcStatusText = $("<span></span>");
743
+ const calcDetailsLink = $("<span></span>")
744
+ .addClass("calculation-details-link")
745
+ .attr("title", i18next.t('factDetails.viewCalculationDetails'))
746
+ .text("details")
747
+ .click((e) => {
748
+ const dialog = new CalculationInspector();
749
+ dialog.displayCalculation(rCalc);
750
+ dialog.show();
751
+ e.stopPropagation();
752
+ })
753
+ const calcStatus = $("<p></p>")
754
+ .append(calcStatusText)
755
+ .append($("<span></span>").text(" ("))
756
+ .append(calcDetailsLink)
757
+ .append($("<span></span>").text(")"))
722
758
  .appendTo(calcBody);
759
+ if (rCalc.binds()) {
760
+ if (rCalc.isConsistent()) {
761
+ calcStatusIcon
762
+ .addClass("consistent-flag")
763
+ .attr("title", i18next.t('factDetails.calculationIsConsistent'))
764
+ calcStatusText.text(i18next.t('factDetails.calculationIsConsistent'));
765
+ }
766
+ else {
767
+ calcStatusIcon
768
+ .addClass("inconsistent-flag")
769
+ .attr("title", i18next.t('factDetails.calculationIsInconsistent'))
770
+ calcStatusText.text(i18next.t('factDetails.calculationIsInconsistent'));
771
+ }
772
+ }
773
+ else if (rCalc.unchecked()) {
774
+ calcStatusIcon
775
+ .addClass("unchecked-flag")
776
+ .attr("title", i18next.t('factDetails.calculationUnchecked'))
777
+ calcStatusText.text(i18next.t('factDetails.calculationUnchecked'));
778
+ }
723
779
 
724
- a.addCard($("<span></span>").text(label), calcBody, e == elr);
725
-
780
+ a.addCard(cardTitle, calcBody, rCalc.elr == selectedELR);
726
781
  }
727
782
  return a.contents();
728
783
  }
@@ -813,7 +868,7 @@ export class Inspector {
813
868
  }
814
869
  }
815
870
  else {
816
- s = $("<i>").text("n/a").attr("title", "non-numeric fact");
871
+ s = $("<i>").text("n/a").attr("title", i18next.t('factDetails.nonNumericFact'));
817
872
  }
818
873
  $(".fact-properties tr.change td").html(s);
819
874
 
@@ -0,0 +1,70 @@
1
+ // See COPYRIGHT.md for copyright information
2
+
3
+ import Decimal from 'decimal.js';
4
+
5
+ /*
6
+ * Class for working with numeric intervals, of the form: [a, b]
7
+ *
8
+ * Interval is closed (includes both bounds)
9
+ * a must be <= b
10
+ */
11
+ export class Interval {
12
+
13
+ constructor(a, b) {
14
+ this.a = typeof a == 'object' ? a : new Decimal(a);
15
+ this.b = typeof b == 'object' ? b : new Decimal(b);
16
+ }
17
+
18
+ static fromFact(fact) {
19
+ if (!fact.isNumeric()) {
20
+ return undefined;
21
+ }
22
+ const decimals = fact.decimals();
23
+ let width = 0;
24
+ if (decimals !== undefined) {
25
+ const x = new Decimal(10);
26
+ width = x.pow(0-decimals).times(0.5);
27
+ }
28
+ let value;
29
+ const factValue = fact.value();
30
+ try {
31
+ value = new Decimal(factValue);
32
+ } catch (e) {
33
+ if (e instanceof Error && /DecimalError/.test(e.message)) {
34
+ return undefined;
35
+ }
36
+ throw e;
37
+ }
38
+ return new Interval(value.minus(width), value.plus(width));
39
+ }
40
+
41
+ intersection(other) {
42
+ return Interval.intersection(this, other);
43
+ }
44
+
45
+ static intersection(...intervals) {
46
+ if (intervals.includes(undefined) || intervals.length == 0) {
47
+ return undefined;
48
+ }
49
+ const aa = intervals.map(x => x.a);
50
+ const bb = intervals.map(x => x.b);
51
+ const a = Decimal.max(...aa);
52
+ const b = Decimal.min(...bb);
53
+ if (b.lessThan(a)) {
54
+ return undefined;
55
+ }
56
+ return new Interval(a, b);
57
+ }
58
+
59
+ plus(other) {
60
+ return new Interval(this.a.plus(other.a), this.b.plus(other.b));
61
+ }
62
+
63
+ times(x) {
64
+ return x > 0 ? new Interval(this.a.times(x), this.b.times(x)) : new Interval(this.b.times(x), this.a.times(x));
65
+ }
66
+
67
+ midpoint() {
68
+ return Decimal.add(this.a, this.b).div(2);
69
+ }
70
+ }
@@ -0,0 +1,153 @@
1
+ // Copyright 2019 Workiva Inc.
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+
15
+ import { Fact } from "./fact.js";
16
+ import { Interval } from "./interval.js";
17
+ import { ReportSet } from "./reportset.js";
18
+ import Decimal from 'decimal.js';
19
+ import { viewerUniqueId } from "./util.js";
20
+ import './test-utils.js';
21
+
22
+ function testReportSet(facts) {
23
+ const reportset = new ReportSet({
24
+ prefixes: {},
25
+ concepts: {},
26
+ facts: facts
27
+ });
28
+ reportset._initialize();
29
+ return reportset;
30
+ }
31
+
32
+ function testFact(factData) {
33
+ factData.a = factData.a || {};
34
+ factData.a.c = factData.a.c || 'eg:Concept1';
35
+ if (!('u' in factData.a)) {
36
+ factData.a.u = 'eg:pure';
37
+ }
38
+ else if (factData.a.u === undefined) {
39
+ delete factData.a.u;
40
+ }
41
+ return testReportSet({"f1": factData}).getItemById(viewerUniqueId(0, "f1"));
42
+ }
43
+
44
+ describe("From facts", () => {
45
+ test("Infinite precision", () => {
46
+ var i = Interval.fromFact(testFact({v: 20}));
47
+ expect(i.a).toEqualDecimal(20);
48
+ expect(i.b).toEqualDecimal(20);
49
+
50
+ i = Interval.fromFact(testFact({v: -20}));
51
+ expect(i.a).toEqualDecimal(-20);
52
+ expect(i.b).toEqualDecimal(-20);
53
+ });
54
+
55
+ test("Finite precision", () => {
56
+ var i = Interval.fromFact(testFact({v: 20, d: 0}));
57
+ expect(i.a).toEqualDecimal(19.5);
58
+ expect(i.b).toEqualDecimal(20.5);
59
+
60
+ i = Interval.fromFact(testFact({v: -20.123, d: 3}));
61
+ expect(i.a).toEqualDecimal(-20.1235);
62
+ expect(i.b).toEqualDecimal(-20.1225);
63
+
64
+ i = Interval.fromFact(testFact({v: -20.123, d: -1}));
65
+ expect(i.a).toEqualDecimal(-25.123);
66
+ expect(i.b).toEqualDecimal(-15.123);
67
+ });
68
+
69
+ test("Non numeric", () => {
70
+ var i = Interval.fromFact(testFact({v: 20, a: {u: undefined}}));
71
+ expect(i).toBeUndefined();
72
+
73
+ // This would be invalid XBRL
74
+ i = Interval.fromFact(testFact({v: "abc" }));
75
+ expect(i).toBeUndefined();
76
+ });
77
+ });
78
+
79
+
80
+ describe("Intersect", () => {
81
+ test("Intersecting", () => {
82
+ var x = new Interval(5, 15);
83
+ var y = new Interval(10, 25);
84
+ var ii = x.intersection(y);
85
+ expect(ii.a).toEqualDecimal(10);
86
+ expect(ii.b).toEqualDecimal(15);
87
+
88
+ ii = Interval.intersection(x, y);
89
+ expect(ii.a).toEqualDecimal(10);
90
+ expect(ii.b).toEqualDecimal(15);
91
+
92
+ ii = y.intersection(x);
93
+ expect(ii.a).toEqualDecimal(10);
94
+ expect(ii.b).toEqualDecimal(15);
95
+
96
+ y = new Interval(15, 25);
97
+ ii = x.intersection(y);
98
+ expect(ii.a).toEqualDecimal(15);
99
+ expect(ii.b).toEqualDecimal(15);
100
+
101
+ x = new Interval(10, 20);
102
+ y = new Interval(15, 25);
103
+ var z = new Interval(18, 30);
104
+ ii = Interval.intersection(x, y, z);
105
+ expect(ii.a).toEqualDecimal(18);
106
+ expect(ii.b).toEqualDecimal(20);
107
+
108
+ });
109
+
110
+ test("No intersection", () => {
111
+ var x = new Interval(5, 15);
112
+ expect(x.intersection(new Interval(20, 25))).toBeUndefined();
113
+ expect(x.intersection(new Interval(1, 4.99))).toBeUndefined();
114
+
115
+ x = new Interval(10, 20);
116
+ var y = new Interval(15, 25);
117
+ var z = new Interval(21, 30);
118
+ expect(Interval.intersection(x, y, z)).toBeUndefined();
119
+ });
120
+ })
121
+
122
+ describe("Arithmetic", () => {
123
+ test("Add", () => {
124
+ var i = new Interval(5, 15);
125
+ var ii = i.plus(new Interval(10, 20));
126
+ expect(ii.a).toEqualDecimal(15);
127
+ expect(ii.b).toEqualDecimal(35);
128
+ });
129
+
130
+ test("Multiply", () => {
131
+ var i = new Interval(5, 15);
132
+ var ii = i.times(2);
133
+ expect(ii.a).toEqualDecimal(10);
134
+ expect(ii.b).toEqualDecimal(30);
135
+
136
+ ii = i.times(-2);
137
+ expect(ii.a).toEqualDecimal(-30);
138
+ expect(ii.b).toEqualDecimal(-10);
139
+
140
+ i = new Interval(-10, -5);
141
+ ii = i.times(2);
142
+ expect(ii.a).toEqualDecimal(-20);
143
+ expect(ii.b).toEqualDecimal(-10);
144
+
145
+ ii = i.times(-2);
146
+ expect(ii.a).toEqualDecimal(10);
147
+ expect(ii.b).toEqualDecimal(20);
148
+
149
+ });
150
+
151
+ });
152
+
153
+
@@ -11,6 +11,25 @@ export function TestInspector() {
11
11
 
12
12
  TestInspector.prototype = Object.create(Inspector.prototype);
13
13
 
14
+ expect.extend({
15
+ toEqualDecimal(received, expected) {
16
+ const options = {
17
+ comment: 'decimal.js equality',
18
+ isNot: this.isNot,
19
+ promise: this.promise,
20
+ };
21
+ const pass = received.equals(expected);
22
+ const message = () =>
23
+ this.utils.matcherHint('toEqualDecimals', undefined, undefined, options) +
24
+ '\n\n' +
25
+ `Expected: ${this.isNot ? '(not) ' : ''}${this.utils.printExpected(new Decimal(expected))}\n` +
26
+ `Received: ${this.utils.printReceived(received)}`;
27
+
28
+ return {actual: received, message, pass};
29
+
30
+ }
31
+ });
32
+
14
33
  export function createSimpleFact(id, concept, options=null) {
15
34
  options = options || {};
16
35
  return {
@@ -271,14 +271,12 @@ export class Viewer {
271
271
  /* Otherwise, insert a <span> as wrapper */
272
272
  if (nodes.length == 0) {
273
273
  nodes.push(this._wrapNode(domNode));
274
-
275
- // Create a list of the wrapper node, and all absolutely positioned
276
- // descendants.
277
- for (const e of domNode.querySelectorAll("*")) {
278
- if (getComputedStyle(e).getPropertyValue('position') === "absolute") {
279
- nodes.push(e);
280
- }
281
- }
274
+ }
275
+ // Create a list of the wrapper node, and all absolutely positioned descendants.
276
+ for (const e of nodes[0].querySelectorAll("*")) {
277
+ if (getComputedStyle(e).getPropertyValue('position') === "absolute") {
278
+ nodes.push(e);
279
+ }
282
280
  }
283
281
  for (const [i, n] of nodes.entries()) {
284
282
  // getBoundingClientRect blocks on layout, so only do it if we've actually got absolute nodes