edf2csv 0.8.97 → 0.8.98
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.
- package/dist/cli/report.js +2 -29
- package/dist/cli/report.js.map +1 -1
- package/dist/convert/plan.js +2 -2
- package/dist/convert/plan.js.map +1 -1
- package/dist/convert/timing.js +5 -5
- package/dist/convert/timing.js.map +1 -1
- package/dist/edf/header.js +2 -2
- package/dist/edf/header.js.map +1 -1
- package/dist/format/number.d.ts +1 -0
- package/dist/format/number.js +34 -1
- package/dist/format/number.js.map +1 -1
- package/package.json +1 -1
package/dist/convert/plan.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plan.js","sourceRoot":"","sources":["../../src/convert/plan.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AACpF,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACpE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAC7D,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACjE,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAClG,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC9D,OAAO,EAAE,mBAAmB,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAgHpE,MAAM,SAAS,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;AAE9C,oFAAoF;AACpF,MAAM,CAAC,MAAM,qBAAqB,GAAG,SAAS,CAAC;AAE/C,MAAM,UAAU,SAAS,CAAC,KAAgB,EAAE,UAAuB,EAAE;IACnE,oFAAoF;IACpF,yEAAyE;IACzE,aAAa,CAAC,OAAO,CAAC,CAAC;IACvB,2FAA2F;IAC3F,0FAA0F;IAC1F,kCAAkC;IAClC,eAAe,CAAC,KAAK,CAAC,CAAC;IAEvB,MAAM,WAAW,GAAiB,EAAE,CAAC;IACrC,MAAM,WAAW,GAAG,gBAAgB,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IAEpD,uFAAuF;IACvF,4FAA4F;IAC5F,0FAA0F;IAC1F,6FAA6F;IAC7F,gDAAgD;IAChD;;;;;;;;;;;;;MAaE;IACF,MAAM,YAAY,GAAG,CAAC,OAAO,CAAC,MAAM,IAAI,MAAM,CAAC,KAAK,MAAM,CAAC;IAC3D,MAAM,YAAY,GAAG,OAAO,CAAC,eAAe,KAAK,IAAI,CAAC;IACtD,MAAM,YAAY,GAAG,aAAa,CAAC,UAAU,EAAE,OAAO,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC;IACtE,KAAK,MAAM,MAAM,IAAI,kBAAkB,CAAC,KAAK,CAAC,OAAO,EAAE,WAAW,CAAC,EAAE,CAAC;QACpE,MAAM,KAAK,GACT,MAAM,CAAC,KAAK,KAAK,WAAW;YAC1B,CAAC,CAAC,2DAA2D;YAC7D,CAAC,CAAC,8DAA8D,CAAC;QACrE;;;;;;;;;;;;UAYE;QACF,MAAM,KAAK,GAAG,CAAC,YAAY;YACzB,CAAC,CAAC,gBAAgB,WAAW,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,YAAY,gBAAgB;YACnF,CAAC,CAAC,YAAY;gBACZ,CAAC,CAAC,gBAAgB,WAAW,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,yBAAyB;gBACxE,CAAC,CAAC,kBAAkB,WAAW,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC;QACzD,WAAW,CAAC,IAAI,CAAC;YACf,IAAI,EAAE,iBAAiB;YACvB,QAAQ,EAAE,SAAS;YACnB,OAAO,EAAE,UAAU,MAAM,CAAC,KAAK,iBAAiB,MAAM,CAAC,KAAK,eAAe,KAAK,QAAQ,KAAK,GAAG;YAChG;;;;;;;cAOE;YACF,IAAI,EACF,YAAY,IAAI,CAAC,YAAY;gBAC3B,CAAC,CAAC,qDAAqD,YAAY,uBAAuB;gBAC1F,CAAC,CAAC,oDAAoD,YAAY,uBAAuB;SAC9F,CAAC,CAAC;IACL,CAAC;IAED,MAAM,KAAK,GAAG,YAAY,CAAC;QACzB,KAAK,EAAE,OAAO,CAAC,KAAK;QACpB,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,QAAQ,EAAE,OAAO,CAAC,QAAQ;QAC1B,YAAY,EAAE,OAAO,CAAC,YAAY;QAClC,GAAG,EAAE,OAAO,CAAC,GAAG;QAChB,OAAO,EAAE,OAAO,CAAC,OAAO;QACxB,cAAc,EAAE,KAAK,CAAC,cAAc;QACpC,WAAW,EAAE,KAAK,CAAC,WAAW;QAC9B,YAAY,EAAE,KAAK,CAAC,YAAY;KACjC,CAAC,CAAC;IAGH,IAAI,MAAM,GAAgB,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC;IAExE;;;;;;;;;MASE;IACF,IAAI,OAAO,CAAC,QAAQ,IAAI,OAAO,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACpD,MAAM,SAAS,GAAG,cAAc,CAAC,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC;QAClE,IAAI,YAAY;YAAE,MAAM,GAAG,SAAS,CAAC,OAAO,CAAC;QAC7C,KAAK,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,SAAS,CAAC,SAAS,EAAE,CAAC;YACpD,WAAW,CAAC,IAAI,CAAC;gBACf,IAAI,EAAE,iBAAiB;gBACvB,QAAQ,EAAE,SAAS;gBACnB,OAAO,EACL,IAAI,IAAI,aAAa,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,SAAS,CAAC,cAAc;oBACrE,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,+BAA+B;gBAC7E,IAAI,EAAE,oBAAoB,OAAO,CAAC,CAAC,CAAC,EAAE,KAAK,IAAI,CAAC,qBAAqB;aACtE,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,MAAM,CAAC;IACxC,MAAM,MAAM,GAAG,YAAY;QACzB,CAAC,CAAC,WAAW,CAAC,MAAM,EAAE,WAAW,EAAE,OAAO,CAAC,QAAQ,EAAE,OAAO,CAAC,IAAI,KAAK,IAAI,EAAE,MAAM,CAAC;QACnF,CAAC,CAAC,EAAE,CAAC;IACP,MAAM,QAAQ,GAAG,cAAc,CAC7B,MAAM,EACN,KAAK,EACL,KAAK,CAAC,cAAc,EACpB,KAAK,CAAC,YAAY,EAClB,OAAO,CAAC,GAAG,KAAK,IAAI,EACpB,MAAM,CACP,CAAC;IAEF;;;;;;;;;;;;MAYE;IACF,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACtB,WAAW,CAAC,IAAI,CAAC;YACf,IAAI,EAAE,sBAAsB;YAC5B,QAAQ,EAAE,SAAS;YACnB,OAAO,EACL,gBAAgB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,yBAAyB,CAAC,GAAG;gBACpE,IAAI,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,IAAI;YAC9E,IAAI,EACF,MAAM,KAAK,MAAM;gBACf,CAAC,CAAC,mFAAmF;gBACrF,CAAC,CAAC,mEAAmE;SAC1E,CAAC,CAAC;IACL,CAAC;IAED;;;;;;;;;;;;;;;;;MAiBE;IACF;;;;;;;;;;;;;MAaE;IACF,MAAM,WAAW,GAAG,CAAC,KAAgB,EAAU,EAAE,CAC/C,OAAO,CAAC,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC;IAEnE,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;QACjD;;;;;;;;;;;;;;;UAeE;QACF,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;YACjC,WAAW,CAAC,IAAI,CAAC;gBACf,IAAI,EAAE,iBAAiB;gBACvB,QAAQ,EAAE,SAAS;gBACnB,OAAO,EACL,eAAe,WAAW,CAAC,KAAK,CAAC,mCAAmC,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM;oBAChG,kFAAkF;oBAClF,0EAA0E;gBAC5E,IAAI,EACF,gFAAgF;oBAChF,gEAAgE;aACnE,CAAC,CAAC;QACL,CAAC;aAAM,IAAI,IAAI,GAAG,CAAC,IAAI,IAAI,GAAG,EAAE,IAAI,CAAC,KAAK,CAAC,YAAY,EAAE,CAAC;YACxD,WAAW,CAAC,IAAI,CAAC;gBACf,IAAI,EAAE,iBAAiB;gBACvB,QAAQ,EAAE,SAAS;gBACnB,OAAO,EACL,eAAe,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,6CAA6C;oBAClF,uCAAuC,WAAW,CAAC,KAAK,CAAC,+BAA+B;gBAC1F;;;;;;;;;;;;;;;kBAeE;gBACF,IAAI,EACF,mFAAmF;oBACnF,8EAA8E;oBAC9E,oDAAoD;aACvD,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED;;;;;;;;;;;;;;;;;;;;MAoBE;IACF,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,MAAM,KAAK,GAAG,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,kBAAkB,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;QACzE,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;YAAE,SAAS;QACjC,WAAW,CAAC,IAAI,CAAC;YACf,IAAI,EAAE,kBAAkB;YACxB,QAAQ,EAAE,SAAS;YACnB,OAAO,EACL,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,WAAW;gBACzF,mFAAmF;gBACnF,qBAAqB,WAAW,CAAC,KAAK,CAAC,GAAG;YAC5C,IAAI,EACF,kFAAkF;gBAClF,iEAAiE;SACpE,CAAC,CAAC;IACL,CAAC;IAED;;;;;;;;;;;;MAYE;IACF;;;;;MAKE;IACF,MAAM,UAAU,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;IACxE,IAAI,YAAY,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,IAAI,QAAQ,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC;QAC5E,WAAW,CAAC,IAAI,CAAC,WAAW,CAAC,KAAK,EAAE,KAAK,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC;IACzF,CAAC;IAED,IAAI,QAAQ,CAAC,uBAAuB,EAAE,CAAC;QACrC,WAAW,CAAC,IAAI,CAAC;YACf,IAAI,EAAE,cAAc;YACpB,QAAQ,EAAE,SAAS;YACnB,OAAO,EACL,gDAAgD,OAAO,CAAC,qBAAqB,CAAC,GAAG;gBACjF,qDAAqD;YACvD,IAAI,EAAE,qFAAqF;SAC5F,CAAC,CAAC;IACL,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,KAAK,IAAI,EAAE,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,WAAW,EAAE,QAAQ,EAAE,CAAC;AAClH,CAAC;AAED;;;;;;GAMG;AACH,SAAS,WAAW,CAClB,OAA6B,EAC7B,WAAgC,EAChC,cAAkC,EAClC,IAAa,EACb,MAAuB;IAEvB,MAAM,MAAM,GAAG,IAAI,GAAG,EAAuB,CAAC;IAC9C,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,wEAAwE;QACxE,2EAA2E;QAC3E,IAAI,MAAM,CAAC,gBAAgB,KAAK,CAAC;YAAE,SAAS;QAC5C,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;QAC/C,IAAI,MAAM;YAAE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;;YAC3B,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,YAAY,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC;IACjD,CAAC;IAED,MAAM,KAAK,GAAG,CAAC,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IACvD,oFAAoF;IACpF,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,MAAM,KAAK,MAAM,CAAC;IAEvD;;;;;;;;;;;;;;;;;MAiBE;IACF,MAAM,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC;IACzC,MAAM,KAAK,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IAC9C,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,MAAM,UAAU,GAAG,CAAC,KAAa,EAAU,EAAE;QAC3C,MAAM,IAAI,GAAG,WAAW,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC;QACvC,IAAI,IAAI,GAAG,GAAG,IAAI,GAAG,MAAM,EAAE,CAAC;QAC9B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE;YAAE,IAAI,GAAG,GAAG,IAAI,IAAI,CAAC,GAAG,MAAM,EAAE,CAAC;QACpE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACf,OAAO,IAAI,CAAC;IACd,CAAC,CAAC;IAEF,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE;QAC/B,MAAM,OAAO,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;QACvC,MAAM,KAAK,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;QACzB,OAAO;YACL,IAAI;YACJ,gBAAgB,EAAE,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC;YACpD,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC,UAAU,MAAM,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,KAAK,CAAC;YACzD;;;;cAIE;YACF,YAAY,EAAE,MAAM,KAAK,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,IAAI,CAAC;YAC3F,QAAQ,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;gBACjC,MAAM;gBACN,MAAM,EAAE,WAAW,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,UAAU,MAAM,CAAC,KAAK,EAAE;gBACjE,QAAQ,EAAE,cAAc,IAAI,iBAAiB,CAAC,MAAM,CAAC;aACtD,CAAC,CAAC;SACJ,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,sBAAsB,CAAC,WAAkC;IACvE,OAAO,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,sBAAsB,CAAC,CAAC;AACtE,CAAC;AAED,gEAAgE;AAChE,SAAS,OAAO,CAAC,QAAgB;IAC/B,OAAO,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,IAAI,CAAC;AAC3C,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,QAAQ,CAAC,IAAY;IACnC,wFAAwF;IACxF,8BAA8B;IAC9B,OAAO,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC;AACnC,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,aAAa,CAAC,IAAY,EAAE,IAAa;IACvD,OAAO,GAAG,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;AAC/C,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,SAAS,YAAY,CAAC,KAAoB,EAAE,QAAgB;IAC1D,OAAO,OAAO,CACZ,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,YAAY,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,EAClE,QAAQ,EACR,KAAK,CAAC,YAAY,GAAG,CAAC,IAAI,KAAK,CAAC,UAAU,GAAG,CAAC,CAC/C,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,SAAS,YAAY,CAAC,OAAuB;IAC3C,MAAM,KAAK,GAAG,UAAU,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IACzC,MAAM,KAAK,GACT,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;QAClD,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC;IACrD,IAAI,KAAK;QAAE,OAAO,CAAC,CAAC;IACpB,OAAO,OAAO,CACZ,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,WAAW,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,EACpF,OAAO,CAAC,QAAQ,EAChB,OAAO,CAAC,MAAM,CAAC,WAAW,GAAG,CAAC,IAAI,OAAO,CAAC,MAAM,CAAC,WAAW,GAAG,CAAC,CACjE,CAAC;AACJ,CAAC;AAED,wFAAwF;AACxF,MAAM,iBAAiB,GAAG,GAAG,CAAC;AAE9B,mFAAmF;AACnF,SAAS,OAAO,CAAC,SAAiB,EAAE,QAAgB,EAAE,MAAM,GAAG,KAAK;IAClE,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IACjC,MAAM,IAAI,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC5B,MAAM,QAAQ,GAAG,QAAQ,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;IAEjD;;;;;;;;;MASE;IACF;;;;;;;;;;;;;MAaE;IACF,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,GAAG,iBAAiB,GAAG,QAAQ,CAAC;IACvE,IAAI,IAAI,GAAG,IAAI;QAAE,OAAO,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC;IAC5E,OAAO,IAAI,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,QAAQ,CAAC;AAC9D,CAAC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,SAAS,WAAW,CAClB,KAAoB,EACpB,WAAmB,EACnB,MAA4B,EAC5B,IAAa;IAEb,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC;IAChC,MAAM,KAAK,GAAG,CAAC,KAAK,CAAC,gBAAgB,CAAC;IACtC;;;;;;;;;;;;;;MAcE;IACF,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC;IAC1F,MAAM,QAAQ,GAAG,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;IAC/C,MAAM,KAAK,GAAG,KAAK,CAAC,UAAU,GAAG,KAAK,CAAC,YAAY,CAAC;IACpD,MAAM,MAAM,GAAG,KAAK,IAAI,QAAQ,GAAG,CAAC,IAAI,KAAK,GAAG,CAAC,IAAI,KAAK,GAAG,QAAQ,CAAC;IACtE;;;;;MAKE;IACF,MAAM,MAAM,GACV,SAAS,KAAK,CAAC;QACb,CAAC,CAAC,8CAA8C;QAChD,CAAC,CAAC,iDAAiD,CAAC;IACxD,OAAO;QACL,IAAI,EAAE,cAAc;QACpB,QAAQ,EAAE,SAAS;QACnB,OAAO,EAAE,KAAK;YACZ,CAAC,CAAC,gDAAgD,KAAK,CAAC,KAAK,CAAC,YAAY,EAAE,CAAC,CAAC,OAAO;gBACnF,GAAG,KAAK,CAAC,KAAK,CAAC,UAAU,EAAE,CAAC,CAAC,UAAU,MAAM,GAAG;YAClD,CAAC,CAAC,oBAAoB,OAAO,CAAC,WAAW,EAAE,aAAa,CAAC,8BAA8B;gBACrF,MAAM,MAAM,GAAG;QACnB,IAAI,EAAE,KAAK;YACT,CAAC,CAAC;;;;;;;;;;gBAUE;gBACF,KAAK,CAAC,UAAU,IAAI,KAAK,CAAC,qBAAqB;oBAC/C,CAAC,CAAC,4BAA4B,KAAK,CAAC,KAAK,CAAC,qBAAqB,EAAE,CAAC,CAAC,kBAAkB;wBACnF,mFAAmF;wBACnF,sCAAsC;oBACxC,CAAC,CAAC,MAAM;wBACN,CAAC,CAAC,SAAS,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,oDAAoD;4BAC5E,GAAG,KAAK,CAAC,QAAQ,CAAC,6DAA6D;4BAC/E,iFAAiF;4BACjF,sBAAsB;wBACxB,CAAC,CAAC;;;;;;;;;;;;4BAYE;4BACF,iFAAiF;gCACjF,gFAAgF;gCAChF,kEAAkE;YACxE,CAAC,CAAC,qEAAqE;gBACrE,kFAAkF;oBAClF,0CAA0C,aAAa,CAAC,UAAU,EAAE,IAAI,CAAC,GAAG;KACjF,CAAC;AACJ,CAAC;AAED,SAAS,cAAc,CACrB,MAA4B,EAC5B,KAAoB,EACpB,cAAsB,EACtB,YAA6C,EAC7C,GAAY,EACZ,MAAuB;IAEvB,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,IAAI,OAAO,GAAG,KAAK,CAAC;IACpB,oFAAoF;IACpF,sFAAsF;IACtF,IAAI,QAAQ,GAAG,CAAC,CAAC;IAEjB,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,IAAI,SAAS,GAAG,CAAC,CAAC;QAClB,KAAK,IAAI,MAAM,GAAG,KAAK,CAAC,WAAW,EAAE,MAAM,GAAG,KAAK,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,CAAC;YACxE,MAAM,WAAW,GAAG,YAAY;gBAC9B,CAAC,CAAC,CAAC,YAAY,CAAC,MAAM,CAAC,IAAI,MAAM,GAAG,cAAc,CAAC;gBACnD,CAAC,CAAC,MAAM,GAAG,cAAc,CAAC;YAC5B,SAAS,IAAI,mBAAmB,CAAC;gBAC/B,WAAW;gBACX,IAAI,EAAE,KAAK,CAAC,IAAI;gBAChB,gBAAgB,EAAE,KAAK,CAAC,gBAAgB;gBACxC,YAAY,EAAE,KAAK,CAAC,YAAY;gBAChC,UAAU,EAAE,KAAK,CAAC,UAAU;aAC7B,CAAC,CAAC;QACL,CAAC;QACD,IAAI,MAAM,KAAK,MAAM,EAAE,CAAC;YACtB,kEAAkE;YAClE,MAAM,UAAU,GAAG,SAAS,GAAG,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC;YACrD,IAAI,IAAI,UAAU,CAAC;YACnB,QAAQ,IAAI,UAAU,CAAC;YACvB;;;;cAIE;YACF,MAAM,SAAS,GAAG,YAAY,CAAC,KAAK,EAAE,KAAK,CAAC,YAAY,CAAC,CAAC;YAC1D,KAAK,MAAM,OAAO,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;gBACrC,MAAM,UAAU,GAAG,YAAY,CAAC,OAAO,CAAC,CAAC;gBACzC,MAAM,SAAS,GAAG,MAAM,CAAC,UAAU,CAAC,cAAc,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;gBACpE,8BAA8B;gBAC9B,KAAK,IAAI,SAAS,GAAG,CAAC,SAAS,GAAG,SAAS,GAAG,UAAU,GAAG,CAAC,CAAC,CAAC;YAChE,CAAC;YACD,SAAS;QACX,CAAC;QAED,IAAI,IAAI,SAAS,CAAC;QAClB,IAAI,SAAS,GAAG,CAAC,GAAG,qBAAqB;YAAE,OAAO,GAAG,IAAI,CAAC;QAE1D;;;;;;;;;;;;;;;;UAgBE;QACF,MAAM,SAAS,GAAG,YAAY,CAAC,KAAK,EAAE,KAAK,CAAC,YAAY,CAAC,CAAC;QAC1D,MAAM,SAAS,GAAG,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,YAAY,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QAC9E,2CAA2C;QAC3C,KAAK,IAAI,SAAS,GAAG,CAAC,SAAS,GAAG,SAAS,GAAG,KAAK,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QACzE;;;;;;;;;;;UAWE;QACF,KAAK,IAAI,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,GAAG,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;QAC3F,iFAAiF;QACjF,yFAAyF;QACzF,qFAAqF;QACrF,qBAAqB;QACrB,IAAI,GAAG;YAAE,KAAK,IAAI,SAAS,CAAC;IAC9B,CAAC;IAED,IAAI,MAAM,KAAK,MAAM,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC3C,IAAI,QAAQ,GAAG,CAAC,GAAG,qBAAqB;YAAE,OAAO,GAAG,IAAI,CAAC;QACzD,KAAK,IAAI,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;QACvE,IAAI,GAAG;YAAE,KAAK,IAAI,SAAS,CAAC;IAC9B,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,uBAAuB,EAAE,OAAO,EAAE,CAAC;AAC3D,CAAC","sourcesContent":["/**\n * Turning a request into a concrete conversion plan.\n *\n * The plan is where the tool's central promise is enforced: channels recorded at\n * different sampling rates are never merged into one table. A single wide CSV can\n * only hold mixed rates by inventing samples for the slow channels — MNE, for\n * instance, expands three genuine 1 Hz temperature readings into 768 interpolated\n * values without warning. Instead each distinct rate gets its own file, so every\n * number in every output file is a number that was actually recorded.\n */\n\nimport type { Diagnostic } from '../edf/errors.js';\nimport type { EdfSignal } from '../edf/header.js';\nimport { formatRate, formatRates } from '../edf/header.js';\nimport { decimalsAreClamped, decimalsForSignal, makeScaler } from '../edf/scale.js';\nimport { UTF8_BOM, csvRow, escapeCsvField } from '../format/csv.js';\nimport { counted, grouped, listed } from '../format/list.js';\nimport { fixed, plain, timeDecimals } from '../format/number.js';\nimport { TIME_COLUMN, buildColumnNames, renamedByCollision, selectChannels } from './channels.js';\nimport { assertOptions, assertPlanInput } from './options.js';\nimport { countSamplesInRange, resolveRange } from './time-range.js';\nimport type { ResolvedRange } from './time-range.js';\n\nexport interface PlannedChannel {\n signal: EdfSignal;\n column: string;\n decimals: number;\n}\n\nexport interface RateGroup {\n /** Sampling rate in Hz shared by every channel in this group. */\n rate: number;\n samplesPerRecord: number;\n fileName: string;\n timeDecimals: number;\n channels: PlannedChannel[];\n}\n\nexport interface PlanInput {\n signals: readonly EdfSignal[];\n recordDuration: number;\n recordCount: number;\n hasAnnotationChannel: boolean;\n /**\n * True start time of each data record, supplied for discontinuous files. The\n * requested time window is resolved against these rather than against\n * `recordCount * recordDuration`, which for a file with gaps is the amount of\n * data rather than the span of time it covers.\n */\n recordStarts?: Float64Array | null | undefined;\n}\n\nexport interface PlanOptions {\n channels?: readonly string[] | undefined;\n start?: number | undefined;\n /** The `--start` value exactly as typed, for error messages. */\n startText?: string | undefined;\n duration?: number | undefined;\n /** The `--duration` value exactly as typed, for error messages. */\n durationText?: string | undefined;\n end?: number | undefined;\n /** The `--end` value exactly as typed, for error messages. */\n endText?: string | undefined;\n annotationsOnly?: boolean | undefined;\n /** Force a fixed number of decimals instead of deriving it per channel. */\n decimals?: number | undefined;\n /** Compress each CSV with gzip, giving every one of them a `.gz` name. */\n gzip?: boolean | undefined;\n /** Start each CSV with a UTF-8 byte order mark, so Excel reads it as UTF-8. */\n bom?: boolean | undefined;\n /**\n * How the samples are arranged in the CSV.\n *\n * `'wide'`, the default, gives one column per channel and one file per sampling rate.\n * `'long'` gives one file, three columns — `time_s`, `channel`, `value` — and one row per\n * sample. See ConversionPlan.layout for why that is the only way to put channels recorded\n * at different rates in one table without inventing samples.\n */\n layout?: 'wide' | 'long' | undefined;\n /**\n * Whether the one table this produces goes to stdout rather than to a directory.\n *\n * Read only by the warnings that name where their rows land. Nothing about the plan itself\n * changes — `--stdout` is refused unless the recording makes exactly one table — but three\n * of those warnings named `signals.csv`, a file such a run never writes.\n */\n toStdout?: boolean | undefined;\n}\n\nexport interface ConversionPlan {\n groups: RateGroup[];\n /**\n * How the samples are arranged. `'wide'` is a column per channel and a file per rate;\n * `'long'` is `time_s,channel,value`, one row per sample, all rates in one file.\n *\n * The wide layout has to split a mixed-rate recording across files: a 100 Hz channel and\n * a 1 Hz channel share no rows, and putting them in one wide table means either 99 empty\n * cells out of every hundred or inventing the samples that would fill them. In the long\n * layout each sample carries its own time, so nothing has to line up and nothing is\n * invented — which also makes it the one layout `--stdout` can stream for such a file.\n */\n layout: 'wide' | 'long';\n\n /**\n * Whether the CSVs will be compressed.\n *\n * Recorded rather than inferred from the group file names. Under `--annotations-only`\n * there are no groups to read it off, and `--info` named `annotations.csv` for a run that\n * wrote `annotations.csv.gz`.\n */\n gzip: boolean;\n range: ResolvedRange;\n columnNames: Map<number, string>;\n writeSignals: boolean;\n diagnostics: Diagnostic[];\n estimate: OutputEstimate;\n}\n\nexport interface OutputEstimate {\n /** Total data rows across every signal file. */\n rows: number;\n /**\n * Approximate size of the signal CSVs as CSV text, which under `gzip` is not their size on\n * disk: what is counted here is what the compressor is handed, and the file holds what it\n * produces. `--info` writes \"before compression\" beside this number for that reason, and\n * `infoJson` calls it a character count.\n */\n bytes: number;\n /** True when any single file would exceed Excel's row limit. */\n exceedsSpreadsheetLimit: boolean;\n}\n\nconst BOM_BYTES = Buffer.byteLength(UTF8_BOM);\n\n/** Excel and most spreadsheet tools stop at 1,048,576 rows including the header. */\nexport const SPREADSHEET_ROW_LIMIT = 1_048_576;\n\nexport function buildPlan(input: PlanInput, options: PlanOptions = {}): ConversionPlan {\n // First, and before a directory is created or a stream opened, so a rejected option\n // leaves nothing behind. See assertOptions for what used to get through.\n assertOptions(options);\n // And the argument in front of it, which carries the numbers every figure below is derived\n // from — and was never looked at. See assertPlanInput for what a plan made of two missing\n // ones said about the conversion.\n assertPlanInput(input);\n\n const diagnostics: Diagnostic[] = [];\n const columnNames = buildColumnNames(input.signals);\n\n // A channel whose own label was taken by something else in the header. Usually that is\n // another channel's disambiguating suffix — the duplicate-label warning is about the labels\n // that collided, this is about the channel that lost its name to them. The other way is a\n // channel labelled `time_s`, where what took the name is the time column itself, which every\n // signals.csv begins with and no file supplies.\n /*\n \"so its column is\" was a sentence about the wide layout, printed in both.\n\n A long signals.csv has three columns — time_s, channel, value — and none of them is a\n label: a channel appears there as a value in the `channel` column. So a `--layout long`\n run was told that a column had been renamed to avoid colliding with a column neither of\n them has, under a hint promising that \"column names are unique\" about a set of three\n fixed strings the file never got from the header.\n\n The rename is right in both layouts, which is why only the noun moves. The names have to\n agree between the `channel` cells and channels.csv and across runs, and the `pivot` the\n documentation gives for turning a long table back into a wide one would otherwise put a\n `time_s` column against a `time_s` index — the collision one step later.\n */\n const inLongLayout = (options.layout ?? 'wide') === 'long';\n const writeSignals = options.annotationsOnly !== true;\n const channelsFile = outputCsvName('channels', options.gzip === true);\n for (const signal of renamedByCollision(input.signals, columnNames)) {\n const taker =\n signal.label === TIME_COLUMN\n ? 'the name of the time column every signals.csv starts with'\n : `also the column name another channel's \"_ch\" suffix produces`;\n /*\n And the third place a renamed channel can be named, which is the only one left when no\n signal table is written at all.\n\n 0.8.24 gave this sentence a long-layout branch, because a long signals.csv names a\n channel in its `channel` column rather than in a column of its own. `--annotations-only`\n writes neither: it writes channels.csv and annotations.csv, and the rename shows up in\n the `column` cell of the first. So the mode that never gets a signal table was told its\n column had been renamed, by a run that wrote no column anywhere.\n\n The rename still happens and still matters — the names have to agree with channels.csv\n and across runs, which is the reason the paragraph above gives.\n */\n const named = !writeSignals\n ? `it is named \"${columnNames.get(signal.index)}\" in ${channelsFile}'s column cell`\n : inLongLayout\n ? `it is named \"${columnNames.get(signal.index)}\" in the channel column`\n : `its column is \"${columnNames.get(signal.index)}\"`;\n diagnostics.push({\n code: 'DUPLICATE_LABEL',\n severity: 'warning',\n message: `Signal ${signal.index} is labelled \"${signal.label}\", which is ${taker}, so ${named}.`,\n /*\n And the name that file is written under, which `--gzip` changes.\n\n `outputCsvName` exists because \"`--info` named `annotations.csv` for a run that wrote\n `annotations.csv.gz`\", and 0.8.48 took the two warnings in `run.ts` through it. These\n two hints send a reader to a file by name — it is the whole advice — and named the one\n a `--gzip` run does not write, four lines above a summary listing `channels.csv.gz`.\n */\n hint:\n inLongLayout || !writeSignals\n ? `Channel names are unique; look this channel up in ${channelsFile} by its signal_index.`\n : `Column names are unique; look this channel up in ${channelsFile} by its signal_index.`,\n });\n }\n\n const range = resolveRange({\n start: options.start,\n startText: options.startText,\n duration: options.duration,\n durationText: options.durationText,\n end: options.end,\n endText: options.endText,\n recordDuration: input.recordDuration,\n recordCount: input.recordCount,\n recordStarts: input.recordStarts,\n });\n\n\n let chosen: EdfSignal[] = input.signals.filter((s) => !s.isAnnotations);\n\n /*\n Channel names are checked even under --annotations-only, where the selection is not\n otherwise used.\n\n Skipping the check meant `--channels TYPO --annotations-only` exited 0 in silence while\n the same typo without the flag was a usage error, and `--channels \"\"` stayed an error\n in both — so a mistyped name was the one form of bad input the tool accepted quietly.\n Everywhere else a term matching nothing is reported rather than ignored; a flag that\n happens not to apply is a poor reason to make an exception.\n */\n if (options.channels && options.channels.length > 0) {\n const selection = selectChannels(input.signals, options.channels);\n if (writeSignals) chosen = selection.signals;\n for (const { term, matched } of selection.ambiguous) {\n diagnostics.push({\n code: 'DUPLICATE_LABEL',\n severity: 'warning',\n message:\n `\"${term}\" matches ${counted(matched.length, 'channel')} (positions ` +\n `${listed(matched.map((s) => `#${s.index}`))}); all of them were selected.`,\n hint: `Use --channels \"#${matched[0]?.index ?? 0}\" to pick just one.`,\n });\n }\n }\n\n const layout = options.layout ?? 'wide';\n const groups = writeSignals\n ? groupByRate(chosen, columnNames, options.decimals, options.gzip === true, layout)\n : [];\n const estimate = estimateOutput(\n groups,\n range,\n input.recordDuration,\n input.recordStarts,\n options.bom === true,\n layout,\n );\n\n /*\n The mixed-rate warning describes what this conversion does, not what the file holds.\n\n The header parser raises its own, which is right for `parseHeader` — but it sees every\n channel and knows nothing about `--channels`. Converting one channel out of a three-rate\n recording therefore announced \"3 different sampling rates ... written to one file per\n rate\" over a run that wrote one file, in the same output where `--info` had already\n marked the other two \"(not selected)\". Selecting two of the three was wrong the other\n way: still \"3\".\n\n Callers combining these with a file's own diagnostics drop that copy in favour of this\n one; see `withoutFileRateWarning`.\n */\n if (groups.length > 1) {\n diagnostics.push({\n code: 'MIXED_SAMPLING_RATES',\n severity: 'warning',\n message:\n `Channels use ${counted(groups.length, 'different sampling rate')} ` +\n `(${listed(formatRates(groups.map((g) => g.rate)).map((r) => `${r} Hz`))}).`,\n hint:\n layout === 'long'\n ? 'They share one table, each row carrying its own time, so no channel is resampled.'\n : 'They are written to one file per rate so no channel is resampled.',\n });\n }\n\n /*\n A time column that cannot tell two samples apart.\n\n Sample times are written to at most fifteen decimal places, which separates everything a\n terminating rate can reach — every power of two through 32768 Hz and far past it. Below\n that interval the column repeats: at 3e15 Hz, whose reciprocal never terminates, the rows\n of one record carry the same time_s, so joining or plotting on it silently collapses\n samples that are genuinely distinct. Nothing is lost from the file — every sample is\n there, in order — but the column stops being an identifier, and that is worth saying\n rather than leaving to be discovered.\n\n This said nine places and a gigahertz, and illustrated it with 1 ns records — all three of\n which stopped being true when 0.4.55 raised the search bound. Fifteen places resolve\n 1e-15 s, so a nanosecond interval is written exactly and that recording raises nothing at\n all. warnings-and-errors.md was corrected then and carries a note saying so; the comment\n it was written from was not, and the hint printed four lines below here has said \"the\n fifteen places a double can hold exactly\" ever since.\n */\n /*\n Where the rows a warning is about actually land.\n\n Three warnings below name `group.fileName`, which is the file the wide layout writes — and\n under `--stdout` no file is written at all. `repeating-fast.edf --info --stdout` printed\n both halves two lines apart:\n\n 0 ch1 ch1 uV 3000000000000000 Hz -100 to 100 (stdout)\n warning: Channels at 3000000000000000 Hz sample faster than the time column can\n distinguish, so consecutive rows in signals.csv carry the same time_s value.\n\n The OUTPUT column has said `(stdout)` since 0.8.31. The warning under it did not, so a\n reader is told the rows go to the terminal and then sent to look in a file for them.\n */\n const destination = (group: RateGroup): string =>\n options.toStdout === true ? 'the CSV on stdout' : group.fileName;\n\n for (const group of groups) {\n const step = group.rate > 0 ? 1 / group.rate : 0;\n /*\n The limit of the same failure, which read as the absence of it.\n\n `samplesPerRecord / recordDuration` is a double, and a record duration of 1e-308 with\n four samples in it is Infinity. `1 / Infinity` is 0, so `step > 0` was false and this\n said nothing — while every sample was dropped, the run exited 0, and the only warning\n printed was EMPTY_WINDOW's \"This recording's 2 data records carry no samples in range\",\n which is untrue twice over: the records carry eight samples and no range was asked for.\n\n One power of ten away, at 1e-300, the rate is 4e300 and the file converts with the\n warning below. Same guard `decimalsAreClamped` had before 0.5.83, in the column next\n door: a step of exactly zero means no resolution at all, not nothing to report.\n\n Its own branch because the hint below is false here — no rows are written at all, so\n \"Every sample is written, in order\" would be the third untrue sentence.\n */\n if (!Number.isFinite(group.rate)) {\n diagnostics.push({\n code: 'TIME_RESOLUTION',\n severity: 'warning',\n message:\n `Channels in ${destination(group)} work out to a sampling rate of ${formatRate(group.rate)} Hz ` +\n `— their samples per record over a record duration too small to divide into — so ` +\n `their samples cannot be placed in time and no rows are written for them.`,\n hint:\n 'Check the record duration in the header. One power of ten larger and the same ' +\n 'file converts, with consecutive rows carrying the same time_s.',\n });\n } else if (step > 0 && step < 10 ** -group.timeDecimals) {\n diagnostics.push({\n code: 'TIME_RESOLUTION',\n severity: 'warning',\n message:\n `Channels at ${formatRate(group.rate)} Hz sample faster than the time column can ` +\n `distinguish, so consecutive rows in ${destination(group)} carry the same time_s value.`,\n /*\n \"or convert one rate at a time with --channels\" was advice that does nothing.\n\n It parses, it runs, it exits 0, and the warning comes back word for word — so\n somebody who followed it had every reason to think the column had been fixed. In\n the wide layout each rate already has its own file and its own precision, and\n `timeDecimals` is a function of the rate alone, so a narrowed conversion writes the\n same column it wrote before. In the long layout the shared column takes the finest\n precision *in the conversion*, and dropping rates can only make it coarser — never\n fine enough to separate samples that were already inseparable.\n\n Nor is there another option that would: --decimals sets the value precision and\n says so, and every rate that reaches this warning has already been given the\n fifteen places that are the ceiling. The first sentence was the whole of the\n answer, so it is the whole of the hint.\n */\n hint:\n 'Every sample is written, in order. Use the row number rather than time_s to tell ' +\n 'them apart: the column already carries the fifteen places a double can hold ' +\n 'exactly, so no option or selection separates them.',\n });\n }\n }\n\n /*\n The same failure as TIME_RESOLUTION, one column over.\n\n A channel whose quantization step is below 1e-98 needs more decimals than `toFixed` can\n print, so consecutive digital codes round to the same text and the arithmetic the FAQ\n gives for recovering them stops working. That used to happen at 1e-20 and silently — see\n MAX_DERIVED_DECIMALS. It is rare now, but \"rare\" is the reason to say so rather than the\n reason not to.\n\n Asked of the ceiling, not of the precision in use, and so asked whatever `--decimals`\n says. `--decimals 2` on a channel needing 3 is a trade the caller made knowingly, and\n reporting it was reporting the flag back at the person who typed it — every channel of an\n ordinary EEG raised this, and since --strict turns any diagnostic into exit 1,\n `--decimals 2 --strict` could not succeed on any recording at all.\n\n 0.5.10 fixed that by skipping the check whenever `--decimals` was given, which suppressed\n the real case along with the false one: at `--decimals 20` a channel stepping by 1e-106\n printed every code it had as `0.00000000000000000000`, and said nothing. The question is\n not who chose the precision. It is whether any precision the tool can print would\n separate consecutive codes.\n */\n for (const group of groups) {\n const short = group.channels.filter((c) => decimalsAreClamped(c.signal));\n if (short.length === 0) continue;\n diagnostics.push({\n code: 'VALUE_RESOLUTION',\n severity: 'warning',\n message:\n `${listed(short.map((c) => c.column))} ${short.length === 1 ? 'steps' : 'step'} by less ` +\n `than any number of decimals this can print, so some consecutive samples round to ` +\n `the same value in ${destination(group)}.`,\n hint:\n 'Every sample is written, in order, and the physical values are computed at full ' +\n 'precision either way. What is lost is only in the printed text.',\n });\n }\n\n /*\n A window that selects nothing is a fact about the plan, so the plan is where it is raised.\n\n It was pushed by `convert()` from the rows actually written, which meant `--info` never\n said it: `--info --start 0.31 --end 0.39` on a 10 Hz recording printed \"Would write 0\n rows\" with no warning and exited 0 under `--strict`, while converting the same window\n warned and exited 1. The hint says \"Run with --info to see where the records actually\n sit\" — advising the reader into the one mode that would not tell them.\n\n The estimate's row count is exact, which `npm run estimate` checks across every fixture\n crossed with every option set, so raising it from the plan says the same thing the rows\n would have.\n */\n /*\n Not when a rate above already explained it. EMPTY_WINDOW says the records \"carry no\n samples in range\", and on a recording whose rate overflowed to Infinity that is untrue\n twice: the records carry their samples, and no range was asked for. The rate warning is\n the accurate account of the same zero.\n */\n const untimeable = groups.some((group) => !Number.isFinite(group.rate));\n if (writeSignals && groups.length > 0 && estimate.rows === 0 && !untimeable) {\n diagnostics.push(emptyWindow(range, input.recordCount, groups, options.gzip === true));\n }\n\n if (estimate.exceedsSpreadsheetLimit) {\n diagnostics.push({\n code: 'LARGE_OUTPUT',\n severity: 'warning',\n message:\n `At least one output file will have more than ${grouped(SPREADSHEET_ROW_LIMIT)} ` +\n `rows, which is more than Excel or Numbers can open.`,\n hint: 'Use --start and --duration to convert a section, or read the file with pandas or R.',\n });\n }\n\n return { groups, layout, gzip: options.gzip === true, range, columnNames, writeSignals, diagnostics, estimate };\n}\n\n/**\n * Partition channels by sampling rate, largest first.\n *\n * The common case — every channel at one rate — collapses to a single group and a\n * single `signals.csv`, so the honest behaviour costs nothing when there is nothing\n * to be honest about.\n */\nfunction groupByRate(\n signals: readonly EdfSignal[],\n columnNames: Map<number, string>,\n forcedDecimals: number | undefined,\n gzip: boolean,\n layout: 'wide' | 'long',\n): RateGroup[] {\n const byRate = new Map<number, EdfSignal[]>();\n for (const signal of signals) {\n // A channel with no samples has no sampling rate to group by, and would\n // otherwise produce an empty \"0hz\" file. The header parser already warned.\n if (signal.samplesPerRecord === 0) continue;\n const bucket = byRate.get(signal.samplingRate);\n if (bucket) bucket.push(signal);\n else byRate.set(signal.samplingRate, [signal]);\n }\n\n const rates = [...byRate.keys()].sort((a, b) => b - a);\n // The long layout writes one table whatever the rates are, so every group names it.\n const single = rates.length === 1 || layout === 'long';\n\n /*\n Two distinct rates can produce the same slug, because the slug rounds to six decimal\n places. Rates come from samplesPerRecord / recordDuration and every channel shares the\n record duration, so the closest two rates can be is 1 / recordDuration — which drops\n below 1e-6 once a record is longer than about eleven days. Absurd, but the header\n permits it, and the failure was silent and destructive: both groups opened a write\n stream on the same path, so the file ended up holding interleaved rows from both\n channels under a header naming only one of them.\n\n Distinct rates therefore get distinct files, always. The suffix is only ever reached by\n a collision, so ordinary recordings keep the names they have always had.\n\n Naming from the whole set of rates at once removes most of those collisions before the\n suffix has to. Rounding each rate on its own gave 1e-6 Hz and 1.25e-6 Hz the same slug,\n and the numbering below then produced signals_0_000001hz.csv and signals_0_000001hz_2.csv\n — two files that no longer overwrite each other, but of which only one is named for the\n rate it holds. The suffix stays as the backstop for anything this still cannot separate.\n */\n const suffix = gzip ? '.csv.gz' : '.csv';\n const slugs = formatRates(rates).map(slugFor);\n const used = new Set<string>();\n const uniqueName = (index: number): string => {\n const base = `signals_${slugs[index]}`;\n let name = `${base}${suffix}`;\n for (let n = 2; used.has(name); n++) name = `${base}_${n}${suffix}`;\n used.add(name);\n return name;\n };\n\n return rates.map((rate, index) => {\n const members = byRate.get(rate) ?? [];\n const first = members[0];\n return {\n rate,\n samplesPerRecord: first ? first.samplesPerRecord : 0,\n fileName: single ? `signals${suffix}` : uniqueName(index),\n /*\n In the long layout every rate shares a `time_s` column, so they share its precision:\n the finest any of them needs. Writing 100 Hz at three places and 256 Hz at eight in\n the same column would make the column's meaning depend on the row.\n */\n timeDecimals: layout === 'long' ? Math.max(...rates.map(timeDecimals)) : timeDecimals(rate),\n channels: members.map((signal) => ({\n signal,\n column: columnNames.get(signal.index) ?? `signal_${signal.index}`,\n decimals: forcedDecimals ?? decimalsForSignal(signal),\n })),\n };\n });\n}\n\n/**\n * A file's diagnostics with the header's mixed-rate warning removed.\n *\n * `buildPlan` raises that warning for the channels actually being converted, so keeping both\n * would either duplicate it or contradict it. The header parser's copy stays where it is, for\n * callers reading a header without planning a conversion.\n */\nexport function withoutFileRateWarning(diagnostics: readonly Diagnostic[]): Diagnostic[] {\n return diagnostics.filter((d) => d.code !== 'MIXED_SAMPLING_RATES');\n}\n\n/** `256hz`, `12_5hz` — safe in a filename on every platform. */\nfunction slugFor(rendered: string): string {\n return `${rendered.replace('.', '_')}hz`;\n}\n\n/**\n * One rate's slug, rendered on its own.\n *\n * The names a conversion writes come from `formatRates` over the whole set, which widens the\n * precision until rates that differ read as differing — so on a recording carrying both,\n * 1e-6 Hz and 1.25e-6 Hz are `0_000001hz` and `0_00000125hz`. This renders one rate with no\n * set to separate it from, and both of those come back `0_000001hz`.\n *\n * Which is right for the question it is asked, and was worth saying: a caller reaching for the\n * exported slug function to predict a filename got a name the tool does not write, on exactly\n * the rates the reference warns `formatRate` collapses. The two now spell a rendered rate the\n * same way, through the line above, so only the rendering differs and nothing can drift.\n */\nexport function rateSlug(rate: number): string {\n // `formatRate` checks the argument; the two spell a rendered rate the same way, so they\n // refuse the same values too.\n return slugFor(formatRate(rate));\n}\n\n/**\n * The name a CSV is written under, which `--gzip` changes.\n *\n * `plan.gzip` is recorded rather than inferred because \"`--info` named `annotations.csv` for a\n * run that wrote `annotations.csv.gz`\" — and the warnings *about* those files went on doing it\n * after the file list stopped. A run that says \"annotations.csv holds its header and no rows\"\n * three lines above a summary listing `annotations.csv.gz` names two files and writes one.\n *\n * Written once here because three places already computed it by hand.\n */\nexport function outputCsvName(base: string, gzip: boolean): string {\n return `${base}${gzip ? '.csv.gz' : '.csv'}`;\n}\n\n/**\n * Characters the time column occupies, measured over both ends of the window and signed.\n *\n * Both estimates measured the column against `range.endSeconds` alone, unsigned — while the\n * value column two lines below already allowed for a sign when either bound is negative. A\n * recording timed from before zero prints `-100.000` where that budgeted for `100.000`, so\n * every row came out a byte short: 203 predicted against 216 written, and 131 against 159 on\n * a shorter one. An estimate reading low is the one direction the correctness page says it\n * never goes — \"no byte count under the truth\" is what the estimate sweep asserts over every\n * fixture, and no fixture began before zero.\n *\n * The far end is not always the widest: from -100s to -97s it is the start.\n */\nfunction timeWidthFor(range: ResolvedRange, decimals: number): number {\n return widthOf(\n Math.max(Math.abs(range.startSeconds), Math.abs(range.endSeconds)),\n decimals,\n range.startSeconds < 0 || range.endSeconds < 0,\n );\n}\n\n/**\n * How wide a value cell can print, from the channel's own calibration.\n *\n * Zero when that calibration holds no mapping — a digital range of one point, a physical span\n * that overflows or underflows a double — because such a channel writes an empty cell for\n * every sample rather than a number, which is the whole point of `makeScaler` returning NaN.\n *\n * Budgeting a full-width number for one broke the bound this estimate states beside never\n * reading low. A single-channel recording whose digital minimum equals its maximum, at\n * `--decimals 20`, was predicted at 651 bytes and wrote 151 — 4.31x, against a documented wall\n * of three. No fixture reaches it because `degenerate-range.edf` has two ordinary channels\n * beside its flat one, whose real cells pad the total back under the wall.\n *\n * Asked of `makeScaler` rather than restated here, for the reason `csvRow` measures the header\n * row: the function that decides whether a cell gets a number is the one that can still be\n * right when the rule changes. Both ends of the declared range are probed, so a calibration\n * that is finite anywhere keeps its full width and the estimate cannot start reading low.\n */\nfunction valueWidthOf(channel: PlannedChannel): number {\n const scale = makeScaler(channel.signal);\n const blank =\n !Number.isFinite(scale(channel.signal.digitalMin)) &&\n !Number.isFinite(scale(channel.signal.digitalMax));\n if (blank) return 0;\n return widthOf(\n Math.max(Math.abs(channel.signal.physicalMin), Math.abs(channel.signal.physicalMax)),\n channel.decimals,\n channel.signal.physicalMin < 0 || channel.signal.physicalMax < 0,\n );\n}\n\n/** Integer digits in `Number.MAX_VALUE`, which is the widest a finite double prints. */\nconst MAX_DOUBLE_DIGITS = 309;\n\n/** Characters a fixed-decimal number of this magnitude occupies, sign included. */\nfunction widthOf(magnitude: number, decimals: number, signed = false): number {\n const size = Math.abs(magnitude);\n const sign = signed ? 1 : 0;\n const fraction = decimals > 0 ? 1 + decimals : 0;\n\n /*\n Cells are written with toFixed, which rounds. Taking the integer digits from the floor of\n the bound therefore under-counted whenever rounding carried into a new digit: a channel\n bounded at 9999.999 and written to zero decimals produces \"10000\", five characters where\n the floor of 9999.999 suggests four. Every cell on such a channel was a byte short, and\n `--info` reported 127 KB for a file that came out 131 KB.\n\n Measuring the bound as rendered removes that. toFixed switches to exponential notation\n past 1e21, so the arithmetic form still covers magnitudes beyond it.\n */\n /*\n A bound that is not a number bounds nothing, so the widest cell it can produce is taken\n instead: 309 digits, which is `Number.MAX_VALUE` written out.\n\n One digit was budgeted, and the estimate read low — the one direction the correctness page\n says it never goes. `latest` is `recordCount * recordDuration`, so a header stating a\n record duration near the top of a double overflows it while every sample time under it\n stays finite and prints in full. Three records of 1e308, eight samples:\n\n Would write 8 rows, roughly 115 B. signals.csv is 2,244 bytes.\n\n Each of those rows carries a 313-character time cell. `fixed` writes an empty cell for a\n value that is itself non-finite, so nothing wider than this is ever printed.\n */\n if (!Number.isFinite(size)) return sign + MAX_DOUBLE_DIGITS + fraction;\n if (size < 1e21) return sign + size.toFixed(Math.min(decimals, 100)).length;\n return sign + (Math.floor(Math.log10(size)) + 1) + fraction;\n}\n\n/**\n * Raised when the conversion had signal tables to fill and put no data rows in any of them.\n *\n * A window can land where there are no samples without being past the end of the recording:\n * between the last sample and the nominal end of the last record, or — on a discontinuous\n * file — inside a gap. `--start 2 --end 10` on a recording whose records sit at 0s, 1s and\n * 10s asks for eight seconds that contain no data at all.\n *\n * What came out was a signals.csv holding its header and nothing else, exit 0, no warning,\n * and `--strict` passing. The closing summary does say \"signals.csv 0 rows\" and --json\n * carries `rows: 0`, so it was not quite invisible — but a header-only file is exactly what\n * a successful extraction of an empty range looks like, and everywhere else that a request\n * produces nothing this tool says so: a --channels term matching nothing is an error, and\n * --annotations-only on a file with no events raises NO_ANNOTATIONS. A warning rather than\n * an error because a batch of five hundred recordings should not stop for one whose gap\n * happens to line up with the window; --strict turns it into a failure for those who want\n * that.\n */\nfunction emptyWindow(\n range: ResolvedRange,\n recordCount: number,\n groups: readonly RateGroup[],\n gzip: boolean,\n): Diagnostic {\n const fileCount = groups.length;\n const asked = !range.isWholeRecording;\n /*\n A window narrower than one sample interval, which is the commonest way to get here and\n was the one cause the hint did not name.\n\n Sample times sit on a grid of `1 / rate`, so a half-open window at least one interval\n wide always contains one — which makes the converse exact: on a continuous recording, a\n window inside the recording that selects nothing is a window narrower than the interval.\n `--start 0.31 --end 0.39` on a 10 Hz file is 0.08s of a 0.1s grid, and the hint answered\n \"past the last sample, or inside a gap in a discontinuous file\", neither of which is true\n of it — one being a special case of the same thing and the other belonging to a file\n shape this one does not have.\n\n The fastest rate, since that is the one whose grid is finest: if none of its samples fits,\n none of the slower ones does either.\n */\n const fastest = Math.max(...groups.map((group) => group.rate).filter(Number.isFinite), 0);\n const interval = fastest > 0 ? 1 / fastest : 0;\n const width = range.endSeconds - range.startSeconds;\n const narrow = asked && interval > 0 && width > 0 && width < interval;\n /*\n One rate is one file, which is nearly every recording, and this said \"files\" either way:\n \"so the signal files hold their headers and no data\" over a single signals.csv. The count\n is `plan.groups.length` and the caller has had it all along — it is the same number the\n mixed-rate warning three functions up counts to decide whether to fire at all.\n */\n const tables =\n fileCount === 1\n ? 'the signal file holds its header and no data'\n : 'the signal files hold their headers and no data';\n return {\n code: 'EMPTY_WINDOW',\n severity: 'warning',\n message: asked\n ? `No samples fall inside the requested window (${fixed(range.startSeconds, 3)}s to ` +\n `${fixed(range.endSeconds, 3)}s), so ${tables}.`\n : `This recording's ${counted(recordCount, 'data record')} carry no samples in range, ` +\n `so ${tables}.`,\n hint: asked\n ? /*\n Which of the two it was, rather than the second one always.\n\n A recording does not have to start at zero: its first record's timekeeping TAL is\n what it is timed from, so a file whose records begin at 1000s is asked for with\n `--start 1000`. `--start 0 --end 1` on that file was told \"The window is inside the\n recording but lands where there is no data — past the last sample, or inside a gap\n in a discontinuous file\", when the window sits entirely before the recording and\n neither offered explanation applies to it. A start at or past the *end* is already\n an error, so the window being outside can only mean it is before the beginning.\n */\n range.endSeconds <= range.recordingStartSeconds\n ? `This recording starts at ${fixed(range.recordingStartSeconds, 3)}s, so the whole ` +\n 'window sits before it. --start and --end are read on the recording\\'s own clock, ' +\n 'which --info prints as \"Timed from\".'\n : narrow\n ? `It is ${fixed(width, 3)}s wide and the fastest channel here samples every ` +\n `${plain(interval)}s, so no sample time falls inside it. Widen it to at least ` +\n 'one sample interval, or convert more of the recording and take the row nearest ' +\n 'the moment you want.'\n : /*\n Advice that does something in the mode it is printed in.\n\n `EMPTY_WINDOW` is a fact about the plan, and `--info` builds the plan — so\n `discontinuous.edf --info --start 4s --end 4.2s` answered with the command that\n had just printed it. 0.8.42 fixed the same loop in channel selection; these are\n the two hints that were left.\n\n The replacement is also the better answer for a conversion. `--info` does not\n print where the records sit: it prints a count, a duration and a time span that\n \"includes discontinuities\". The record positions are in `time_s`, one per row,\n which is what a conversion without a window writes.\n */\n 'The window is inside the recording but lands where there is no data — inside a ' +\n 'gap in a discontinuous file, or past the last sample. Convert without --start ' +\n 'and --end and read time_s to see where the records actually sit.'\n : // The same loop, in the branch where no window was asked for at all.\n 'What the header declares for each channel — its samples per data record — is in ' +\n `the channel table --info prints and in ${outputCsvName('channels', gzip)}.`,\n };\n}\n\nfunction estimateOutput(\n groups: readonly RateGroup[],\n range: ResolvedRange,\n recordDuration: number,\n recordStarts: Float64Array | null | undefined,\n bom: boolean,\n layout: 'wide' | 'long',\n): OutputEstimate {\n let rows = 0;\n let bytes = 0;\n let exceeds = false;\n // One table in the long layout, so the row limit applies to the sum rather than the\n // largest group, and the header and mark are counted once rather than once per group.\n let longRows = 0;\n\n for (const group of groups) {\n let groupRows = 0;\n for (let record = range.startRecord; record < range.endRecord; record++) {\n const recordStart = recordStarts\n ? (recordStarts[record] ?? record * recordDuration)\n : record * recordDuration;\n groupRows += countSamplesInRange({\n recordStart,\n rate: group.rate,\n samplesPerRecord: group.samplesPerRecord,\n startSeconds: range.startSeconds,\n endSeconds: range.endSeconds,\n });\n }\n if (layout === 'long') {\n // A row per sample per channel rather than a row per sample time.\n const groupCells = groupRows * group.channels.length;\n rows += groupCells;\n longRows += groupCells;\n /*\n `time_s,channel,value`: the time, the channel name as it will be escaped into the\n cell, and the widest the value can print. Same over-counting rule as the wide\n layout — the declared physical range bounds a cell, and most samples sit under it.\n */\n const timeWidth = timeWidthFor(range, group.timeDecimals);\n for (const channel of group.channels) {\n const valueWidth = valueWidthOf(channel);\n const nameWidth = Buffer.byteLength(escapeCsvField(channel.column));\n // Two commas and the newline.\n bytes += groupRows * (timeWidth + nameWidth + valueWidth + 3);\n }\n continue;\n }\n\n rows += groupRows;\n if (groupRows + 1 > SPREADSHEET_ROW_LIMIT) exceeds = true;\n\n /*\n Width per cell, from the channel's own calibration rather than a flat allowance.\n\n The old `decimals + 6` budgeted six characters for the sign, integer part and decimal\n point on every channel, whatever it actually held. That over-counted a millivolt\n channel spanning ±5 by four characters a cell and ran 30-55% high across the fixture\n set — on a number people use to decide whether a conversion is worth starting.\n\n The channel's declared physical range is what bounds a cell, so that bound is what is\n used. Most samples sit below it, so this still reads high, which is the direction a\n size estimate should err in.\n\n One case is outside the bound rather than under it: nothing obliges a recording to keep\n its samples inside the digital range it declares, and one that does not maps outside the\n physical range too. Such a file can convert larger than the estimate. Clamping the data\n to make the estimate true is not a trade worth making — the samples are what they are.\n */\n const timeWidth = timeWidthFor(range, group.timeDecimals);\n const cellWidth = group.channels.reduce((sum, c) => sum + valueWidthOf(c), 0);\n // One comma per channel, plus the newline.\n bytes += groupRows * (timeWidth + cellWidth + group.channels.length + 1);\n /*\n The header row, measured as it will be written rather than as the labels are stored.\n\n A column name is quoted when it contains a comma, a quote, a carriage return or a line\n feed, and every quote inside it is doubled. Counting the raw label under-counted\n that row: three channels labelled `a,b,c,d,e`, `x\"y` and `plain` write a 32-byte header\n and were budgeted 27. EDF labels are free text, so commas in them are ordinary — a montage\n written as `EEG Fpz-Cz, ref` is exactly the kind of thing this is for.\n\n csvRow is the function that writes it, so it is the function that measures it. Nothing\n else is in a position to stay correct when the quoting rules change.\n */\n bytes += Buffer.byteLength(csvRow(['time_s', ...group.channels.map((c) => c.column)])) + 1;\n // Three bytes per file under --bom. Small, but exactly known — unlike the sample\n // overshoot two paragraphs up, which is the one thing here that can read under what gets\n // written and cannot be counted in advance. On a one-row conversion three bytes is a\n // tenth of the file.\n if (bom) bytes += BOM_BYTES;\n }\n\n if (layout === 'long' && groups.length > 0) {\n if (longRows + 1 > SPREADSHEET_ROW_LIMIT) exceeds = true;\n bytes += Buffer.byteLength(csvRow(['time_s', 'channel', 'value'])) + 1;\n if (bom) bytes += BOM_BYTES;\n }\n\n return { rows, bytes, exceedsSpreadsheetLimit: exceeds };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"plan.js","sourceRoot":"","sources":["../../src/convert/plan.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Turning a request into a concrete conversion plan.\n *\n * The plan is where the tool's central promise is enforced: channels recorded at\n * different sampling rates are never merged into one table. A single wide CSV can\n * only hold mixed rates by inventing samples for the slow channels — MNE, for\n * instance, expands three genuine 1 Hz temperature readings into 768 interpolated\n * values without warning. Instead each distinct rate gets its own file, so every\n * number in every output file is a number that was actually recorded.\n */\n\nimport type { Diagnostic } from '../edf/errors.js';\nimport type { EdfSignal } from '../edf/header.js';\nimport { formatRate, formatRates } from '../edf/header.js';\nimport { decimalsAreClamped, decimalsForSignal, makeScaler } from '../edf/scale.js';\nimport { UTF8_BOM, csvRow, escapeCsvField } from '../format/csv.js';\nimport { counted, grouped, listed } from '../format/list.js';\nimport { fixed, plainSeconds, timeDecimals } from '../format/number.js';\nimport { TIME_COLUMN, buildColumnNames, renamedByCollision, selectChannels } from './channels.js';\nimport { assertOptions, assertPlanInput } from './options.js';\nimport { countSamplesInRange, resolveRange } from './time-range.js';\nimport type { ResolvedRange } from './time-range.js';\n\nexport interface PlannedChannel {\n signal: EdfSignal;\n column: string;\n decimals: number;\n}\n\nexport interface RateGroup {\n /** Sampling rate in Hz shared by every channel in this group. */\n rate: number;\n samplesPerRecord: number;\n fileName: string;\n timeDecimals: number;\n channels: PlannedChannel[];\n}\n\nexport interface PlanInput {\n signals: readonly EdfSignal[];\n recordDuration: number;\n recordCount: number;\n hasAnnotationChannel: boolean;\n /**\n * True start time of each data record, supplied for discontinuous files. The\n * requested time window is resolved against these rather than against\n * `recordCount * recordDuration`, which for a file with gaps is the amount of\n * data rather than the span of time it covers.\n */\n recordStarts?: Float64Array | null | undefined;\n}\n\nexport interface PlanOptions {\n channels?: readonly string[] | undefined;\n start?: number | undefined;\n /** The `--start` value exactly as typed, for error messages. */\n startText?: string | undefined;\n duration?: number | undefined;\n /** The `--duration` value exactly as typed, for error messages. */\n durationText?: string | undefined;\n end?: number | undefined;\n /** The `--end` value exactly as typed, for error messages. */\n endText?: string | undefined;\n annotationsOnly?: boolean | undefined;\n /** Force a fixed number of decimals instead of deriving it per channel. */\n decimals?: number | undefined;\n /** Compress each CSV with gzip, giving every one of them a `.gz` name. */\n gzip?: boolean | undefined;\n /** Start each CSV with a UTF-8 byte order mark, so Excel reads it as UTF-8. */\n bom?: boolean | undefined;\n /**\n * How the samples are arranged in the CSV.\n *\n * `'wide'`, the default, gives one column per channel and one file per sampling rate.\n * `'long'` gives one file, three columns — `time_s`, `channel`, `value` — and one row per\n * sample. See ConversionPlan.layout for why that is the only way to put channels recorded\n * at different rates in one table without inventing samples.\n */\n layout?: 'wide' | 'long' | undefined;\n /**\n * Whether the one table this produces goes to stdout rather than to a directory.\n *\n * Read only by the warnings that name where their rows land. Nothing about the plan itself\n * changes — `--stdout` is refused unless the recording makes exactly one table — but three\n * of those warnings named `signals.csv`, a file such a run never writes.\n */\n toStdout?: boolean | undefined;\n}\n\nexport interface ConversionPlan {\n groups: RateGroup[];\n /**\n * How the samples are arranged. `'wide'` is a column per channel and a file per rate;\n * `'long'` is `time_s,channel,value`, one row per sample, all rates in one file.\n *\n * The wide layout has to split a mixed-rate recording across files: a 100 Hz channel and\n * a 1 Hz channel share no rows, and putting them in one wide table means either 99 empty\n * cells out of every hundred or inventing the samples that would fill them. In the long\n * layout each sample carries its own time, so nothing has to line up and nothing is\n * invented — which also makes it the one layout `--stdout` can stream for such a file.\n */\n layout: 'wide' | 'long';\n\n /**\n * Whether the CSVs will be compressed.\n *\n * Recorded rather than inferred from the group file names. Under `--annotations-only`\n * there are no groups to read it off, and `--info` named `annotations.csv` for a run that\n * wrote `annotations.csv.gz`.\n */\n gzip: boolean;\n range: ResolvedRange;\n columnNames: Map<number, string>;\n writeSignals: boolean;\n diagnostics: Diagnostic[];\n estimate: OutputEstimate;\n}\n\nexport interface OutputEstimate {\n /** Total data rows across every signal file. */\n rows: number;\n /**\n * Approximate size of the signal CSVs as CSV text, which under `gzip` is not their size on\n * disk: what is counted here is what the compressor is handed, and the file holds what it\n * produces. `--info` writes \"before compression\" beside this number for that reason, and\n * `infoJson` calls it a character count.\n */\n bytes: number;\n /** True when any single file would exceed Excel's row limit. */\n exceedsSpreadsheetLimit: boolean;\n}\n\nconst BOM_BYTES = Buffer.byteLength(UTF8_BOM);\n\n/** Excel and most spreadsheet tools stop at 1,048,576 rows including the header. */\nexport const SPREADSHEET_ROW_LIMIT = 1_048_576;\n\nexport function buildPlan(input: PlanInput, options: PlanOptions = {}): ConversionPlan {\n // First, and before a directory is created or a stream opened, so a rejected option\n // leaves nothing behind. See assertOptions for what used to get through.\n assertOptions(options);\n // And the argument in front of it, which carries the numbers every figure below is derived\n // from — and was never looked at. See assertPlanInput for what a plan made of two missing\n // ones said about the conversion.\n assertPlanInput(input);\n\n const diagnostics: Diagnostic[] = [];\n const columnNames = buildColumnNames(input.signals);\n\n // A channel whose own label was taken by something else in the header. Usually that is\n // another channel's disambiguating suffix — the duplicate-label warning is about the labels\n // that collided, this is about the channel that lost its name to them. The other way is a\n // channel labelled `time_s`, where what took the name is the time column itself, which every\n // signals.csv begins with and no file supplies.\n /*\n \"so its column is\" was a sentence about the wide layout, printed in both.\n\n A long signals.csv has three columns — time_s, channel, value — and none of them is a\n label: a channel appears there as a value in the `channel` column. So a `--layout long`\n run was told that a column had been renamed to avoid colliding with a column neither of\n them has, under a hint promising that \"column names are unique\" about a set of three\n fixed strings the file never got from the header.\n\n The rename is right in both layouts, which is why only the noun moves. The names have to\n agree between the `channel` cells and channels.csv and across runs, and the `pivot` the\n documentation gives for turning a long table back into a wide one would otherwise put a\n `time_s` column against a `time_s` index — the collision one step later.\n */\n const inLongLayout = (options.layout ?? 'wide') === 'long';\n const writeSignals = options.annotationsOnly !== true;\n const channelsFile = outputCsvName('channels', options.gzip === true);\n for (const signal of renamedByCollision(input.signals, columnNames)) {\n const taker =\n signal.label === TIME_COLUMN\n ? 'the name of the time column every signals.csv starts with'\n : `also the column name another channel's \"_ch\" suffix produces`;\n /*\n And the third place a renamed channel can be named, which is the only one left when no\n signal table is written at all.\n\n 0.8.24 gave this sentence a long-layout branch, because a long signals.csv names a\n channel in its `channel` column rather than in a column of its own. `--annotations-only`\n writes neither: it writes channels.csv and annotations.csv, and the rename shows up in\n the `column` cell of the first. So the mode that never gets a signal table was told its\n column had been renamed, by a run that wrote no column anywhere.\n\n The rename still happens and still matters — the names have to agree with channels.csv\n and across runs, which is the reason the paragraph above gives.\n */\n const named = !writeSignals\n ? `it is named \"${columnNames.get(signal.index)}\" in ${channelsFile}'s column cell`\n : inLongLayout\n ? `it is named \"${columnNames.get(signal.index)}\" in the channel column`\n : `its column is \"${columnNames.get(signal.index)}\"`;\n diagnostics.push({\n code: 'DUPLICATE_LABEL',\n severity: 'warning',\n message: `Signal ${signal.index} is labelled \"${signal.label}\", which is ${taker}, so ${named}.`,\n /*\n And the name that file is written under, which `--gzip` changes.\n\n `outputCsvName` exists because \"`--info` named `annotations.csv` for a run that wrote\n `annotations.csv.gz`\", and 0.8.48 took the two warnings in `run.ts` through it. These\n two hints send a reader to a file by name — it is the whole advice — and named the one\n a `--gzip` run does not write, four lines above a summary listing `channels.csv.gz`.\n */\n hint:\n inLongLayout || !writeSignals\n ? `Channel names are unique; look this channel up in ${channelsFile} by its signal_index.`\n : `Column names are unique; look this channel up in ${channelsFile} by its signal_index.`,\n });\n }\n\n const range = resolveRange({\n start: options.start,\n startText: options.startText,\n duration: options.duration,\n durationText: options.durationText,\n end: options.end,\n endText: options.endText,\n recordDuration: input.recordDuration,\n recordCount: input.recordCount,\n recordStarts: input.recordStarts,\n });\n\n\n let chosen: EdfSignal[] = input.signals.filter((s) => !s.isAnnotations);\n\n /*\n Channel names are checked even under --annotations-only, where the selection is not\n otherwise used.\n\n Skipping the check meant `--channels TYPO --annotations-only` exited 0 in silence while\n the same typo without the flag was a usage error, and `--channels \"\"` stayed an error\n in both — so a mistyped name was the one form of bad input the tool accepted quietly.\n Everywhere else a term matching nothing is reported rather than ignored; a flag that\n happens not to apply is a poor reason to make an exception.\n */\n if (options.channels && options.channels.length > 0) {\n const selection = selectChannels(input.signals, options.channels);\n if (writeSignals) chosen = selection.signals;\n for (const { term, matched } of selection.ambiguous) {\n diagnostics.push({\n code: 'DUPLICATE_LABEL',\n severity: 'warning',\n message:\n `\"${term}\" matches ${counted(matched.length, 'channel')} (positions ` +\n `${listed(matched.map((s) => `#${s.index}`))}); all of them were selected.`,\n hint: `Use --channels \"#${matched[0]?.index ?? 0}\" to pick just one.`,\n });\n }\n }\n\n const layout = options.layout ?? 'wide';\n const groups = writeSignals\n ? groupByRate(chosen, columnNames, options.decimals, options.gzip === true, layout)\n : [];\n const estimate = estimateOutput(\n groups,\n range,\n input.recordDuration,\n input.recordStarts,\n options.bom === true,\n layout,\n );\n\n /*\n The mixed-rate warning describes what this conversion does, not what the file holds.\n\n The header parser raises its own, which is right for `parseHeader` — but it sees every\n channel and knows nothing about `--channels`. Converting one channel out of a three-rate\n recording therefore announced \"3 different sampling rates ... written to one file per\n rate\" over a run that wrote one file, in the same output where `--info` had already\n marked the other two \"(not selected)\". Selecting two of the three was wrong the other\n way: still \"3\".\n\n Callers combining these with a file's own diagnostics drop that copy in favour of this\n one; see `withoutFileRateWarning`.\n */\n if (groups.length > 1) {\n diagnostics.push({\n code: 'MIXED_SAMPLING_RATES',\n severity: 'warning',\n message:\n `Channels use ${counted(groups.length, 'different sampling rate')} ` +\n `(${listed(formatRates(groups.map((g) => g.rate)).map((r) => `${r} Hz`))}).`,\n hint:\n layout === 'long'\n ? 'They share one table, each row carrying its own time, so no channel is resampled.'\n : 'They are written to one file per rate so no channel is resampled.',\n });\n }\n\n /*\n A time column that cannot tell two samples apart.\n\n Sample times are written to at most fifteen decimal places, which separates everything a\n terminating rate can reach — every power of two through 32768 Hz and far past it. Below\n that interval the column repeats: at 3e15 Hz, whose reciprocal never terminates, the rows\n of one record carry the same time_s, so joining or plotting on it silently collapses\n samples that are genuinely distinct. Nothing is lost from the file — every sample is\n there, in order — but the column stops being an identifier, and that is worth saying\n rather than leaving to be discovered.\n\n This said nine places and a gigahertz, and illustrated it with 1 ns records — all three of\n which stopped being true when 0.4.55 raised the search bound. Fifteen places resolve\n 1e-15 s, so a nanosecond interval is written exactly and that recording raises nothing at\n all. warnings-and-errors.md was corrected then and carries a note saying so; the comment\n it was written from was not, and the hint printed four lines below here has said \"the\n fifteen places a double can hold exactly\" ever since.\n */\n /*\n Where the rows a warning is about actually land.\n\n Three warnings below name `group.fileName`, which is the file the wide layout writes — and\n under `--stdout` no file is written at all. `repeating-fast.edf --info --stdout` printed\n both halves two lines apart:\n\n 0 ch1 ch1 uV 3000000000000000 Hz -100 to 100 (stdout)\n warning: Channels at 3000000000000000 Hz sample faster than the time column can\n distinguish, so consecutive rows in signals.csv carry the same time_s value.\n\n The OUTPUT column has said `(stdout)` since 0.8.31. The warning under it did not, so a\n reader is told the rows go to the terminal and then sent to look in a file for them.\n */\n const destination = (group: RateGroup): string =>\n options.toStdout === true ? 'the CSV on stdout' : group.fileName;\n\n for (const group of groups) {\n const step = group.rate > 0 ? 1 / group.rate : 0;\n /*\n The limit of the same failure, which read as the absence of it.\n\n `samplesPerRecord / recordDuration` is a double, and a record duration of 1e-308 with\n four samples in it is Infinity. `1 / Infinity` is 0, so `step > 0` was false and this\n said nothing — while every sample was dropped, the run exited 0, and the only warning\n printed was EMPTY_WINDOW's \"This recording's 2 data records carry no samples in range\",\n which is untrue twice over: the records carry eight samples and no range was asked for.\n\n One power of ten away, at 1e-300, the rate is 4e300 and the file converts with the\n warning below. Same guard `decimalsAreClamped` had before 0.5.83, in the column next\n door: a step of exactly zero means no resolution at all, not nothing to report.\n\n Its own branch because the hint below is false here — no rows are written at all, so\n \"Every sample is written, in order\" would be the third untrue sentence.\n */\n if (!Number.isFinite(group.rate)) {\n diagnostics.push({\n code: 'TIME_RESOLUTION',\n severity: 'warning',\n message:\n `Channels in ${destination(group)} work out to a sampling rate of ${formatRate(group.rate)} Hz ` +\n `— their samples per record over a record duration too small to divide into — so ` +\n `their samples cannot be placed in time and no rows are written for them.`,\n hint:\n 'Check the record duration in the header. One power of ten larger and the same ' +\n 'file converts, with consecutive rows carrying the same time_s.',\n });\n } else if (step > 0 && step < 10 ** -group.timeDecimals) {\n diagnostics.push({\n code: 'TIME_RESOLUTION',\n severity: 'warning',\n message:\n `Channels at ${formatRate(group.rate)} Hz sample faster than the time column can ` +\n `distinguish, so consecutive rows in ${destination(group)} carry the same time_s value.`,\n /*\n \"or convert one rate at a time with --channels\" was advice that does nothing.\n\n It parses, it runs, it exits 0, and the warning comes back word for word — so\n somebody who followed it had every reason to think the column had been fixed. In\n the wide layout each rate already has its own file and its own precision, and\n `timeDecimals` is a function of the rate alone, so a narrowed conversion writes the\n same column it wrote before. In the long layout the shared column takes the finest\n precision *in the conversion*, and dropping rates can only make it coarser — never\n fine enough to separate samples that were already inseparable.\n\n Nor is there another option that would: --decimals sets the value precision and\n says so, and every rate that reaches this warning has already been given the\n fifteen places that are the ceiling. The first sentence was the whole of the\n answer, so it is the whole of the hint.\n */\n hint:\n 'Every sample is written, in order. Use the row number rather than time_s to tell ' +\n 'them apart: the column already carries the fifteen places a double can hold ' +\n 'exactly, so no option or selection separates them.',\n });\n }\n }\n\n /*\n The same failure as TIME_RESOLUTION, one column over.\n\n A channel whose quantization step is below 1e-98 needs more decimals than `toFixed` can\n print, so consecutive digital codes round to the same text and the arithmetic the FAQ\n gives for recovering them stops working. That used to happen at 1e-20 and silently — see\n MAX_DERIVED_DECIMALS. It is rare now, but \"rare\" is the reason to say so rather than the\n reason not to.\n\n Asked of the ceiling, not of the precision in use, and so asked whatever `--decimals`\n says. `--decimals 2` on a channel needing 3 is a trade the caller made knowingly, and\n reporting it was reporting the flag back at the person who typed it — every channel of an\n ordinary EEG raised this, and since --strict turns any diagnostic into exit 1,\n `--decimals 2 --strict` could not succeed on any recording at all.\n\n 0.5.10 fixed that by skipping the check whenever `--decimals` was given, which suppressed\n the real case along with the false one: at `--decimals 20` a channel stepping by 1e-106\n printed every code it had as `0.00000000000000000000`, and said nothing. The question is\n not who chose the precision. It is whether any precision the tool can print would\n separate consecutive codes.\n */\n for (const group of groups) {\n const short = group.channels.filter((c) => decimalsAreClamped(c.signal));\n if (short.length === 0) continue;\n diagnostics.push({\n code: 'VALUE_RESOLUTION',\n severity: 'warning',\n message:\n `${listed(short.map((c) => c.column))} ${short.length === 1 ? 'steps' : 'step'} by less ` +\n `than any number of decimals this can print, so some consecutive samples round to ` +\n `the same value in ${destination(group)}.`,\n hint:\n 'Every sample is written, in order, and the physical values are computed at full ' +\n 'precision either way. What is lost is only in the printed text.',\n });\n }\n\n /*\n A window that selects nothing is a fact about the plan, so the plan is where it is raised.\n\n It was pushed by `convert()` from the rows actually written, which meant `--info` never\n said it: `--info --start 0.31 --end 0.39` on a 10 Hz recording printed \"Would write 0\n rows\" with no warning and exited 0 under `--strict`, while converting the same window\n warned and exited 1. The hint says \"Run with --info to see where the records actually\n sit\" — advising the reader into the one mode that would not tell them.\n\n The estimate's row count is exact, which `npm run estimate` checks across every fixture\n crossed with every option set, so raising it from the plan says the same thing the rows\n would have.\n */\n /*\n Not when a rate above already explained it. EMPTY_WINDOW says the records \"carry no\n samples in range\", and on a recording whose rate overflowed to Infinity that is untrue\n twice: the records carry their samples, and no range was asked for. The rate warning is\n the accurate account of the same zero.\n */\n const untimeable = groups.some((group) => !Number.isFinite(group.rate));\n if (writeSignals && groups.length > 0 && estimate.rows === 0 && !untimeable) {\n diagnostics.push(emptyWindow(range, input.recordCount, groups, options.gzip === true));\n }\n\n if (estimate.exceedsSpreadsheetLimit) {\n diagnostics.push({\n code: 'LARGE_OUTPUT',\n severity: 'warning',\n message:\n `At least one output file will have more than ${grouped(SPREADSHEET_ROW_LIMIT)} ` +\n `rows, which is more than Excel or Numbers can open.`,\n hint: 'Use --start and --duration to convert a section, or read the file with pandas or R.',\n });\n }\n\n return { groups, layout, gzip: options.gzip === true, range, columnNames, writeSignals, diagnostics, estimate };\n}\n\n/**\n * Partition channels by sampling rate, largest first.\n *\n * The common case — every channel at one rate — collapses to a single group and a\n * single `signals.csv`, so the honest behaviour costs nothing when there is nothing\n * to be honest about.\n */\nfunction groupByRate(\n signals: readonly EdfSignal[],\n columnNames: Map<number, string>,\n forcedDecimals: number | undefined,\n gzip: boolean,\n layout: 'wide' | 'long',\n): RateGroup[] {\n const byRate = new Map<number, EdfSignal[]>();\n for (const signal of signals) {\n // A channel with no samples has no sampling rate to group by, and would\n // otherwise produce an empty \"0hz\" file. The header parser already warned.\n if (signal.samplesPerRecord === 0) continue;\n const bucket = byRate.get(signal.samplingRate);\n if (bucket) bucket.push(signal);\n else byRate.set(signal.samplingRate, [signal]);\n }\n\n const rates = [...byRate.keys()].sort((a, b) => b - a);\n // The long layout writes one table whatever the rates are, so every group names it.\n const single = rates.length === 1 || layout === 'long';\n\n /*\n Two distinct rates can produce the same slug, because the slug rounds to six decimal\n places. Rates come from samplesPerRecord / recordDuration and every channel shares the\n record duration, so the closest two rates can be is 1 / recordDuration — which drops\n below 1e-6 once a record is longer than about eleven days. Absurd, but the header\n permits it, and the failure was silent and destructive: both groups opened a write\n stream on the same path, so the file ended up holding interleaved rows from both\n channels under a header naming only one of them.\n\n Distinct rates therefore get distinct files, always. The suffix is only ever reached by\n a collision, so ordinary recordings keep the names they have always had.\n\n Naming from the whole set of rates at once removes most of those collisions before the\n suffix has to. Rounding each rate on its own gave 1e-6 Hz and 1.25e-6 Hz the same slug,\n and the numbering below then produced signals_0_000001hz.csv and signals_0_000001hz_2.csv\n — two files that no longer overwrite each other, but of which only one is named for the\n rate it holds. The suffix stays as the backstop for anything this still cannot separate.\n */\n const suffix = gzip ? '.csv.gz' : '.csv';\n const slugs = formatRates(rates).map(slugFor);\n const used = new Set<string>();\n const uniqueName = (index: number): string => {\n const base = `signals_${slugs[index]}`;\n let name = `${base}${suffix}`;\n for (let n = 2; used.has(name); n++) name = `${base}_${n}${suffix}`;\n used.add(name);\n return name;\n };\n\n return rates.map((rate, index) => {\n const members = byRate.get(rate) ?? [];\n const first = members[0];\n return {\n rate,\n samplesPerRecord: first ? first.samplesPerRecord : 0,\n fileName: single ? `signals${suffix}` : uniqueName(index),\n /*\n In the long layout every rate shares a `time_s` column, so they share its precision:\n the finest any of them needs. Writing 100 Hz at three places and 256 Hz at eight in\n the same column would make the column's meaning depend on the row.\n */\n timeDecimals: layout === 'long' ? Math.max(...rates.map(timeDecimals)) : timeDecimals(rate),\n channels: members.map((signal) => ({\n signal,\n column: columnNames.get(signal.index) ?? `signal_${signal.index}`,\n decimals: forcedDecimals ?? decimalsForSignal(signal),\n })),\n };\n });\n}\n\n/**\n * A file's diagnostics with the header's mixed-rate warning removed.\n *\n * `buildPlan` raises that warning for the channels actually being converted, so keeping both\n * would either duplicate it or contradict it. The header parser's copy stays where it is, for\n * callers reading a header without planning a conversion.\n */\nexport function withoutFileRateWarning(diagnostics: readonly Diagnostic[]): Diagnostic[] {\n return diagnostics.filter((d) => d.code !== 'MIXED_SAMPLING_RATES');\n}\n\n/** `256hz`, `12_5hz` — safe in a filename on every platform. */\nfunction slugFor(rendered: string): string {\n return `${rendered.replace('.', '_')}hz`;\n}\n\n/**\n * One rate's slug, rendered on its own.\n *\n * The names a conversion writes come from `formatRates` over the whole set, which widens the\n * precision until rates that differ read as differing — so on a recording carrying both,\n * 1e-6 Hz and 1.25e-6 Hz are `0_000001hz` and `0_00000125hz`. This renders one rate with no\n * set to separate it from, and both of those come back `0_000001hz`.\n *\n * Which is right for the question it is asked, and was worth saying: a caller reaching for the\n * exported slug function to predict a filename got a name the tool does not write, on exactly\n * the rates the reference warns `formatRate` collapses. The two now spell a rendered rate the\n * same way, through the line above, so only the rendering differs and nothing can drift.\n */\nexport function rateSlug(rate: number): string {\n // `formatRate` checks the argument; the two spell a rendered rate the same way, so they\n // refuse the same values too.\n return slugFor(formatRate(rate));\n}\n\n/**\n * The name a CSV is written under, which `--gzip` changes.\n *\n * `plan.gzip` is recorded rather than inferred because \"`--info` named `annotations.csv` for a\n * run that wrote `annotations.csv.gz`\" — and the warnings *about* those files went on doing it\n * after the file list stopped. A run that says \"annotations.csv holds its header and no rows\"\n * three lines above a summary listing `annotations.csv.gz` names two files and writes one.\n *\n * Written once here because three places already computed it by hand.\n */\nexport function outputCsvName(base: string, gzip: boolean): string {\n return `${base}${gzip ? '.csv.gz' : '.csv'}`;\n}\n\n/**\n * Characters the time column occupies, measured over both ends of the window and signed.\n *\n * Both estimates measured the column against `range.endSeconds` alone, unsigned — while the\n * value column two lines below already allowed for a sign when either bound is negative. A\n * recording timed from before zero prints `-100.000` where that budgeted for `100.000`, so\n * every row came out a byte short: 203 predicted against 216 written, and 131 against 159 on\n * a shorter one. An estimate reading low is the one direction the correctness page says it\n * never goes — \"no byte count under the truth\" is what the estimate sweep asserts over every\n * fixture, and no fixture began before zero.\n *\n * The far end is not always the widest: from -100s to -97s it is the start.\n */\nfunction timeWidthFor(range: ResolvedRange, decimals: number): number {\n return widthOf(\n Math.max(Math.abs(range.startSeconds), Math.abs(range.endSeconds)),\n decimals,\n range.startSeconds < 0 || range.endSeconds < 0,\n );\n}\n\n/**\n * How wide a value cell can print, from the channel's own calibration.\n *\n * Zero when that calibration holds no mapping — a digital range of one point, a physical span\n * that overflows or underflows a double — because such a channel writes an empty cell for\n * every sample rather than a number, which is the whole point of `makeScaler` returning NaN.\n *\n * Budgeting a full-width number for one broke the bound this estimate states beside never\n * reading low. A single-channel recording whose digital minimum equals its maximum, at\n * `--decimals 20`, was predicted at 651 bytes and wrote 151 — 4.31x, against a documented wall\n * of three. No fixture reaches it because `degenerate-range.edf` has two ordinary channels\n * beside its flat one, whose real cells pad the total back under the wall.\n *\n * Asked of `makeScaler` rather than restated here, for the reason `csvRow` measures the header\n * row: the function that decides whether a cell gets a number is the one that can still be\n * right when the rule changes. Both ends of the declared range are probed, so a calibration\n * that is finite anywhere keeps its full width and the estimate cannot start reading low.\n */\nfunction valueWidthOf(channel: PlannedChannel): number {\n const scale = makeScaler(channel.signal);\n const blank =\n !Number.isFinite(scale(channel.signal.digitalMin)) &&\n !Number.isFinite(scale(channel.signal.digitalMax));\n if (blank) return 0;\n return widthOf(\n Math.max(Math.abs(channel.signal.physicalMin), Math.abs(channel.signal.physicalMax)),\n channel.decimals,\n channel.signal.physicalMin < 0 || channel.signal.physicalMax < 0,\n );\n}\n\n/** Integer digits in `Number.MAX_VALUE`, which is the widest a finite double prints. */\nconst MAX_DOUBLE_DIGITS = 309;\n\n/** Characters a fixed-decimal number of this magnitude occupies, sign included. */\nfunction widthOf(magnitude: number, decimals: number, signed = false): number {\n const size = Math.abs(magnitude);\n const sign = signed ? 1 : 0;\n const fraction = decimals > 0 ? 1 + decimals : 0;\n\n /*\n Cells are written with toFixed, which rounds. Taking the integer digits from the floor of\n the bound therefore under-counted whenever rounding carried into a new digit: a channel\n bounded at 9999.999 and written to zero decimals produces \"10000\", five characters where\n the floor of 9999.999 suggests four. Every cell on such a channel was a byte short, and\n `--info` reported 127 KB for a file that came out 131 KB.\n\n Measuring the bound as rendered removes that. toFixed switches to exponential notation\n past 1e21, so the arithmetic form still covers magnitudes beyond it.\n */\n /*\n A bound that is not a number bounds nothing, so the widest cell it can produce is taken\n instead: 309 digits, which is `Number.MAX_VALUE` written out.\n\n One digit was budgeted, and the estimate read low — the one direction the correctness page\n says it never goes. `latest` is `recordCount * recordDuration`, so a header stating a\n record duration near the top of a double overflows it while every sample time under it\n stays finite and prints in full. Three records of 1e308, eight samples:\n\n Would write 8 rows, roughly 115 B. signals.csv is 2,244 bytes.\n\n Each of those rows carries a 313-character time cell. `fixed` writes an empty cell for a\n value that is itself non-finite, so nothing wider than this is ever printed.\n */\n if (!Number.isFinite(size)) return sign + MAX_DOUBLE_DIGITS + fraction;\n if (size < 1e21) return sign + size.toFixed(Math.min(decimals, 100)).length;\n return sign + (Math.floor(Math.log10(size)) + 1) + fraction;\n}\n\n/**\n * Raised when the conversion had signal tables to fill and put no data rows in any of them.\n *\n * A window can land where there are no samples without being past the end of the recording:\n * between the last sample and the nominal end of the last record, or — on a discontinuous\n * file — inside a gap. `--start 2 --end 10` on a recording whose records sit at 0s, 1s and\n * 10s asks for eight seconds that contain no data at all.\n *\n * What came out was a signals.csv holding its header and nothing else, exit 0, no warning,\n * and `--strict` passing. The closing summary does say \"signals.csv 0 rows\" and --json\n * carries `rows: 0`, so it was not quite invisible — but a header-only file is exactly what\n * a successful extraction of an empty range looks like, and everywhere else that a request\n * produces nothing this tool says so: a --channels term matching nothing is an error, and\n * --annotations-only on a file with no events raises NO_ANNOTATIONS. A warning rather than\n * an error because a batch of five hundred recordings should not stop for one whose gap\n * happens to line up with the window; --strict turns it into a failure for those who want\n * that.\n */\nfunction emptyWindow(\n range: ResolvedRange,\n recordCount: number,\n groups: readonly RateGroup[],\n gzip: boolean,\n): Diagnostic {\n const fileCount = groups.length;\n const asked = !range.isWholeRecording;\n /*\n A window narrower than one sample interval, which is the commonest way to get here and\n was the one cause the hint did not name.\n\n Sample times sit on a grid of `1 / rate`, so a half-open window at least one interval\n wide always contains one — which makes the converse exact: on a continuous recording, a\n window inside the recording that selects nothing is a window narrower than the interval.\n `--start 0.31 --end 0.39` on a 10 Hz file is 0.08s of a 0.1s grid, and the hint answered\n \"past the last sample, or inside a gap in a discontinuous file\", neither of which is true\n of it — one being a special case of the same thing and the other belonging to a file\n shape this one does not have.\n\n The fastest rate, since that is the one whose grid is finest: if none of its samples fits,\n none of the slower ones does either.\n */\n const fastest = Math.max(...groups.map((group) => group.rate).filter(Number.isFinite), 0);\n const interval = fastest > 0 ? 1 / fastest : 0;\n const width = range.endSeconds - range.startSeconds;\n const narrow = asked && interval > 0 && width > 0 && width < interval;\n /*\n One rate is one file, which is nearly every recording, and this said \"files\" either way:\n \"so the signal files hold their headers and no data\" over a single signals.csv. The count\n is `plan.groups.length` and the caller has had it all along — it is the same number the\n mixed-rate warning three functions up counts to decide whether to fire at all.\n */\n const tables =\n fileCount === 1\n ? 'the signal file holds its header and no data'\n : 'the signal files hold their headers and no data';\n return {\n code: 'EMPTY_WINDOW',\n severity: 'warning',\n message: asked\n ? `No samples fall inside the requested window (${fixed(range.startSeconds, 3)}s to ` +\n `${fixed(range.endSeconds, 3)}s), so ${tables}.`\n : `This recording's ${counted(recordCount, 'data record')} carry no samples in range, ` +\n `so ${tables}.`,\n hint: asked\n ? /*\n Which of the two it was, rather than the second one always.\n\n A recording does not have to start at zero: its first record's timekeeping TAL is\n what it is timed from, so a file whose records begin at 1000s is asked for with\n `--start 1000`. `--start 0 --end 1` on that file was told \"The window is inside the\n recording but lands where there is no data — past the last sample, or inside a gap\n in a discontinuous file\", when the window sits entirely before the recording and\n neither offered explanation applies to it. A start at or past the *end* is already\n an error, so the window being outside can only mean it is before the beginning.\n */\n range.endSeconds <= range.recordingStartSeconds\n ? `This recording starts at ${fixed(range.recordingStartSeconds, 3)}s, so the whole ` +\n 'window sits before it. --start and --end are read on the recording\\'s own clock, ' +\n 'which --info prints as \"Timed from\".'\n : narrow\n ? `It is ${fixed(width, 3)}s wide and the fastest channel here samples every ` +\n `${plainSeconds(interval)}s, so no sample time falls inside it. Widen it to at least ` +\n 'one sample interval, or convert more of the recording and take the row nearest ' +\n 'the moment you want.'\n : /*\n Advice that does something in the mode it is printed in.\n\n `EMPTY_WINDOW` is a fact about the plan, and `--info` builds the plan — so\n `discontinuous.edf --info --start 4s --end 4.2s` answered with the command that\n had just printed it. 0.8.42 fixed the same loop in channel selection; these are\n the two hints that were left.\n\n The replacement is also the better answer for a conversion. `--info` does not\n print where the records sit: it prints a count, a duration and a time span that\n \"includes discontinuities\". The record positions are in `time_s`, one per row,\n which is what a conversion without a window writes.\n */\n 'The window is inside the recording but lands where there is no data — inside a ' +\n 'gap in a discontinuous file, or past the last sample. Convert without --start ' +\n 'and --end and read time_s to see where the records actually sit.'\n : // The same loop, in the branch where no window was asked for at all.\n 'What the header declares for each channel — its samples per data record — is in ' +\n `the channel table --info prints and in ${outputCsvName('channels', gzip)}.`,\n };\n}\n\nfunction estimateOutput(\n groups: readonly RateGroup[],\n range: ResolvedRange,\n recordDuration: number,\n recordStarts: Float64Array | null | undefined,\n bom: boolean,\n layout: 'wide' | 'long',\n): OutputEstimate {\n let rows = 0;\n let bytes = 0;\n let exceeds = false;\n // One table in the long layout, so the row limit applies to the sum rather than the\n // largest group, and the header and mark are counted once rather than once per group.\n let longRows = 0;\n\n for (const group of groups) {\n let groupRows = 0;\n for (let record = range.startRecord; record < range.endRecord; record++) {\n const recordStart = recordStarts\n ? (recordStarts[record] ?? record * recordDuration)\n : record * recordDuration;\n groupRows += countSamplesInRange({\n recordStart,\n rate: group.rate,\n samplesPerRecord: group.samplesPerRecord,\n startSeconds: range.startSeconds,\n endSeconds: range.endSeconds,\n });\n }\n if (layout === 'long') {\n // A row per sample per channel rather than a row per sample time.\n const groupCells = groupRows * group.channels.length;\n rows += groupCells;\n longRows += groupCells;\n /*\n `time_s,channel,value`: the time, the channel name as it will be escaped into the\n cell, and the widest the value can print. Same over-counting rule as the wide\n layout — the declared physical range bounds a cell, and most samples sit under it.\n */\n const timeWidth = timeWidthFor(range, group.timeDecimals);\n for (const channel of group.channels) {\n const valueWidth = valueWidthOf(channel);\n const nameWidth = Buffer.byteLength(escapeCsvField(channel.column));\n // Two commas and the newline.\n bytes += groupRows * (timeWidth + nameWidth + valueWidth + 3);\n }\n continue;\n }\n\n rows += groupRows;\n if (groupRows + 1 > SPREADSHEET_ROW_LIMIT) exceeds = true;\n\n /*\n Width per cell, from the channel's own calibration rather than a flat allowance.\n\n The old `decimals + 6` budgeted six characters for the sign, integer part and decimal\n point on every channel, whatever it actually held. That over-counted a millivolt\n channel spanning ±5 by four characters a cell and ran 30-55% high across the fixture\n set — on a number people use to decide whether a conversion is worth starting.\n\n The channel's declared physical range is what bounds a cell, so that bound is what is\n used. Most samples sit below it, so this still reads high, which is the direction a\n size estimate should err in.\n\n One case is outside the bound rather than under it: nothing obliges a recording to keep\n its samples inside the digital range it declares, and one that does not maps outside the\n physical range too. Such a file can convert larger than the estimate. Clamping the data\n to make the estimate true is not a trade worth making — the samples are what they are.\n */\n const timeWidth = timeWidthFor(range, group.timeDecimals);\n const cellWidth = group.channels.reduce((sum, c) => sum + valueWidthOf(c), 0);\n // One comma per channel, plus the newline.\n bytes += groupRows * (timeWidth + cellWidth + group.channels.length + 1);\n /*\n The header row, measured as it will be written rather than as the labels are stored.\n\n A column name is quoted when it contains a comma, a quote, a carriage return or a line\n feed, and every quote inside it is doubled. Counting the raw label under-counted\n that row: three channels labelled `a,b,c,d,e`, `x\"y` and `plain` write a 32-byte header\n and were budgeted 27. EDF labels are free text, so commas in them are ordinary — a montage\n written as `EEG Fpz-Cz, ref` is exactly the kind of thing this is for.\n\n csvRow is the function that writes it, so it is the function that measures it. Nothing\n else is in a position to stay correct when the quoting rules change.\n */\n bytes += Buffer.byteLength(csvRow(['time_s', ...group.channels.map((c) => c.column)])) + 1;\n // Three bytes per file under --bom. Small, but exactly known — unlike the sample\n // overshoot two paragraphs up, which is the one thing here that can read under what gets\n // written and cannot be counted in advance. On a one-row conversion three bytes is a\n // tenth of the file.\n if (bom) bytes += BOM_BYTES;\n }\n\n if (layout === 'long' && groups.length > 0) {\n if (longRows + 1 > SPREADSHEET_ROW_LIMIT) exceeds = true;\n bytes += Buffer.byteLength(csvRow(['time_s', 'channel', 'value'])) + 1;\n if (bom) bytes += BOM_BYTES;\n }\n\n return { rows, bytes, exceedsSpreadsheetLimit: exceeds };\n}\n"]}
|
package/dist/convert/timing.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { counted, grouped, listed } from '../format/list.js';
|
|
2
|
-
import {
|
|
2
|
+
import { plainSeconds } from '../format/number.js';
|
|
3
3
|
/**
|
|
4
4
|
* The EDF+D warning's promise, withdrawn when the file cannot keep it.
|
|
5
5
|
*
|
|
@@ -148,9 +148,9 @@ export function deriveRecordStarts(file, annotationData) {
|
|
|
148
148
|
// recording was told to "Mark the file EDF+C" — not a value BDF+ defines — and
|
|
149
149
|
// that it "is read as plain EDF", which it is not.
|
|
150
150
|
message: `This file has an annotation channel stating that its records begin at ` +
|
|
151
|
-
`${
|
|
151
|
+
`${plainSeconds(stated)}s, but its reserved field carries no ${plus}C or ${plus}D ` +
|
|
152
152
|
`marker — so it is read as plain ${base}, time_s counts from zero, and the two ` +
|
|
153
|
-
`disagree by ${
|
|
153
|
+
`disagree by ${plainSeconds(stated)}s.`,
|
|
154
154
|
hint: `annotations.csv keeps the onsets the file gives, so its events and signals.csv ` +
|
|
155
155
|
`are on different clocks. Mark the file ${plus}C, or subtract the offset from ` +
|
|
156
156
|
`the onsets, before joining them.`,
|
|
@@ -470,13 +470,13 @@ function unusableOrigin(origin, file) {
|
|
|
470
470
|
a record duration near the top of a double, multiplied out over three records, leaves it.
|
|
471
471
|
*/
|
|
472
472
|
const away = Number.isFinite(origin)
|
|
473
|
-
? `${
|
|
473
|
+
? `${plainSeconds(origin)}s from its own start date`
|
|
474
474
|
: 'further from its own start date than a number can hold';
|
|
475
475
|
return {
|
|
476
476
|
code: 'DISCONTINUOUS',
|
|
477
477
|
severity: 'warning',
|
|
478
478
|
message: `This recording's timekeeping annotations place it ${away}, which is too far out for ` +
|
|
479
|
-
`its ${
|
|
479
|
+
`its ${plainSeconds(file.header.recordDuration)}s records to be told apart: at that magnitude ` +
|
|
480
480
|
`adding a sample interval leaves the number unchanged.`,
|
|
481
481
|
hint: 'Sample times are written from zero instead, so every row is present and the column ' +
|
|
482
482
|
'increases. Add the onsets in annotations.csv to recover absolute times if you need them.',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"timing.js","sourceRoot":"","sources":["../../src/convert/timing.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Diagnostic } from '../edf/errors.js';\nimport type { EdfFile } from '../edf/reader.js';\nimport { counted, grouped, listed } from '../format/list.js';\nimport { plain } from '../format/number.js';\n\nexport interface AnnotationTimingData {\n recordStarts: (number | null)[];\n malformed: number;\n /** Unreadable TALs in first position, which carry timing rather than an event. */\n malformedTimekeeping?: number;\n malformedTimekeepingWithText?: number;\n}\n\n/**\n * The EDF+D warning's promise, withdrawn when the file cannot keep it.\n *\n * The header parser raises DISCONTINUOUS with the hint \"Each row carries its true recording\n * time, so gaps stay visible instead of being closed\" — which is what an EDF+D conversion\n * does, when the record times can be read. When they cannot, the very next warning in the\n * same run says the opposite: \"Times are written as if the records were contiguous. Any gaps\n * are lost.\" Two warnings, printed together, and the second denies the first.\n *\n * The parser cannot know: whether the starts can be derived is settled here, after the\n * annotation channel has been read. So the hint is amended where the answer is, the same way\n * `withoutFileRateWarning` drops a header diagnostic the plan has superseded.\n */\nexport function withTimingPromiseKept(\n diagnostics: readonly Diagnostic[],\n derived: boolean,\n): Diagnostic[] {\n if (derived) return [...diagnostics];\n return diagnostics.map((d) =>\n d.code === 'DISCONTINUOUS' && d.hint?.includes('gaps stay visible')\n ? {\n ...d,\n hint:\n 'Where its records sit in time is not recorded in this file, so they are written ' +\n 'as if contiguous — see the warning below.',\n }\n : d,\n );\n}\n\n/**\n * Resolve the true start time of every data record.\n *\n * Continuous recordings need no table because their record positions are\n * arithmetic. EDF+D recordings carry their positions in the annotation channel;\n * missing or malformed timekeeping entries are reported before falling back.\n */\nexport function deriveRecordStarts(\n file: EdfFile,\n annotationData: AnnotationTimingData,\n): { starts: Float64Array | null; diagnostics: Diagnostic[] } {\n const diagnostics: Diagnostic[] = [];\n\n if (annotationData.malformed > 0) {\n diagnostics.push({\n code: 'ANNOTATION_DECODE_FAILED',\n severity: 'warning',\n // Through `counted`, with the irregular plural spelled out — which is the case its\n // own comment names and which nothing was using it for.\n message:\n `${counted(annotationData.malformed, 'annotation entry', 'annotation entries')} ` +\n `${annotationData.malformed === 1 ? 'was' : 'were'} unreadable and could not be exported.`,\n hint: 'The rest were exported normally. The file may have been written by a non-conforming tool.',\n });\n }\n\n\n /*\n A timekeeping TAL is not an event, and saying it \"could not be exported\" describes the\n wrong loss twice over.\n\n These were counted among the annotations, so a file with one unreadable timekeeping TAL\n and three good events announced \"1 annotation entry was unreadable and could not be\n exported\" — while exporting all three. Nothing was missing from annotations.csv; what\n went missing was a record's position in time, which the message never mentioned.\n */\n const lostTimekeeping = annotationData.malformedTimekeeping ?? 0;\n const withText = annotationData.malformedTimekeepingWithText ?? 0;\n // The EDF+D branch below raises its own, which names the records and is more specific.\n // Saying both would report one problem twice.\n if (lostTimekeeping > 0 && file.header.continuity !== 'EDF+D') {\n const one = lostTimekeeping === 1;\n diagnostics.push({\n code: 'ANNOTATION_DECODE_FAILED',\n severity: 'warning',\n // Through `counted`, like the four other counts of \"data record\" in this file. This one\n // spelled the noun and the verb by hand — `record${one ? '' : 's'} carr${one ? 'ies' :\n // 'y'}` — while its nearest neighbour, sixty lines down and about the same records,\n // writes `${counted(n, 'data record')} ${one ? 'carries' : 'carry'}`. Same phrase, same\n // file, two spellings, and only one of them can be checked against the helper.\n message:\n `${counted(lostTimekeeping, 'data record')} ${one ? 'carries' : 'carry'} a timekeeping ` +\n `annotation that could not be read, so ${one ? 'it does' : 'they do'} not say where in ` +\n `time ${one ? 'it sits' : 'they sit'}.`,\n hint:\n /*\n \"No event was lost\" was said whatever the entry held.\n\n A first-position TAL states the record's start time and may carry events after it,\n which the specification allows and writers do. When one of those cannot be parsed\n the events go with it: a six-event file came out with two, and the only warning\n about it said in so many words that nothing had gone. Those entries are counted\n among the unreadable ones now, which is the warning printed above this one; this\n sentence keeps to what is true of the entries it is actually about.\n */\n /*\n Grouped, like the count of the same records one line above it.\n\n The message says `counted(lostTimekeeping, 'data record')`; the hint under it counts\n a subset of those records and spelled the number by hand. When the subset is all of\n them — which is the ordinary case, since a writer that cannot state an onset tends\n not to manage it anywhere — the two print the same number two lines apart, one\n grouped and one not:\n\n warning: 1,010 data records carry a timekeeping annotation that could not be\n read, so they do not say where in time they sit.\n 1010 of them also carried event text, which went with them and is\n counted above.\n */\n (withText > 0\n ? `${withText === 1 ? 'One of them' : `${grouped(withText)} of them`} also carried event text, ` +\n `which went with ${withText === 1 ? 'it' : 'them'} and is counted above. A ` +\n `timekeeping annotation itself states a record's start time and is never exported. `\n : 'No event was lost — a timekeeping annotation states a record\\'s start time and ' +\n 'is never exported. ') + 'Times are derived from the records that could be read.',\n });\n }\n\n /*\n A continuous recording's records are contiguous, but the first one need not sit at zero.\n\n EDF+ puts the header's start time and every annotation onset on one origin, and says the\n first data record's timekeeping TAL \"always starts with +0.X\", stating the fraction of a\n second by which that record follows it. Ignoring that fraction timed the samples from 0\n while the events kept their true onsets, so the two ended up on origins half a second\n apart — an event at +0.75 in a 4 Hz recording whose first TAL reads +0.5 landed on sample\n 3 instead of sample 1. The same file marked EDF+D, byte-identical but for the reserved\n field, placed it correctly, which is what gives the omission away.\n\n Records stay contiguous, which is what continuous means: only the origin moves. A first\n TAL of +0 needs no table at all, and that is nearly every file.\n */\n if (file.header.continuity !== 'EDF+D') {\n if (file.header.continuity !== 'EDF+C') {\n /*\n An annotation channel the reserved field never claimed.\n\n Without an `EDF+C` or `EDF+D` marker this is a plain EDF file, so the origin is not\n applied and the samples are timed from zero. The annotation channel is found by label\n rather than by the marker, though, so its events are still read and exported — with\n the onsets the file gives them.\n\n On a file whose timekeeping says the records start at 1000s, that put signals.csv at\n 0.000 and the event at 1000.5 in annotations.csv: two files from one conversion, a\n thousand seconds apart, and nothing said so. output-files promises the opposite —\n \"`onset_s` is on the same clock as `time_s` in the signal files\".\n\n Reported rather than repaired. Which clock is right is not knowable from here: the\n marker says plain EDF and the annotation channel says otherwise, and picking one would\n move either the samples or the events by the origin on a guess.\n */\n const stated = annotationData.recordStarts.find((start) => start !== null) ?? null;\n const base = file.header.isBdf ? 'BDF' : 'EDF';\n const plus = `${base}+`;\n if (stated !== null && Math.abs(stated) > 0) {\n diagnostics.push({\n code: 'MISSING_EDF_PLUS_MARKER',\n severity: 'warning',\n // The markers as this file spells them, which is what the contradiction warning\n // sixty lines down already does and what NO_SIGNAL_CHANNELS was fixed for. A BDF\n // recording was told to \"Mark the file EDF+C\" — not a value BDF+ defines — and\n // that it \"is read as plain EDF\", which it is not.\n message:\n `This file has an annotation channel stating that its records begin at ` +\n `${plain(stated)}s, but its reserved field carries no ${plus}C or ${plus}D ` +\n `marker — so it is read as plain ${base}, time_s counts from zero, and the two ` +\n `disagree by ${plain(stated)}s.`,\n hint:\n `annotations.csv keeps the onsets the file gives, so its events and signals.csv ` +\n `are on different clocks. Mark the file ${plus}C, or subtract the offset from ` +\n `the onsets, before joining them.`,\n });\n }\n return { starts: null, diagnostics };\n }\n\n /*\n The origin comes from whichever record first states one, not from record 0 alone.\n\n Reading only `recordStarts[0]` meant a single unreadable timekeeping TAL threw the\n origin away and timed the whole file from zero — while records 1 and 2, saying plainly\n that they start at 1.5s and 2.5s, went unread. A recording whose records sit at 0.5s,\n 1.5s and 2.5s came out with every sample 0.5s earlier than the file states, against\n annotation onsets that kept their true values. That is precisely the mismatch 0.4.9\n fixed, arriving through the one hole left in it, and the byte-identical EDF+D twin\n timed it correctly, which is what gives it away.\n\n Continuity is what makes this recoverable: record i sits at `origin + i * duration`,\n so any readable record determines the origin for all of them.\n */\n const origin = originOf(annotationData.recordStarts, file.header.recordDuration);\n if (origin === null) return { starts: null, diagnostics };\n\n const contiguous = new Float64Array(file.recordCount);\n for (let i = 0; i < file.recordCount; i++) {\n contiguous[i] = origin + i * file.header.recordDuration;\n }\n\n /*\n A file marked continuous whose own records disagree about it.\n\n Nothing looked at records past the first, so an EDF+C file whose records are in fact\n spread out was timed as though they were contiguous and said nothing. The records are\n being read here anyway, so the contradiction costs nothing to notice — and it is the\n file, not the reader, that has to be wrong for this to fire.\n\n Compared against what the file can express, not for equality. 0.4.41 asked whether the\n two doubles were the same, which they are not: a recording of 0.1s records sitting at\n 0.1, 0.2, 0.3 ... is contiguous by construction, and 0.1 + 2 * 0.1 is\n 0.30000000000000004. Two of its eight records were reported as contradicting\n continuity, on an ordinary file — and under --strict that was a failed run. The\n smallest interval the recording distinguishes is one sample of its fastest channel;\n anything below half of that is arithmetic, not a gap. `canCarry` has already refused\n origins where the double spacing swamps that interval, so the representation error is\n under the tolerance by construction rather than by hope.\n */\n const tolerance = finestInterval(file) / 2;\n const contradicting = annotationData.recordStarts.filter(\n (declared, i) =>\n typeof declared === 'number' && Math.abs(declared - (contiguous[i] as number)) > tolerance,\n ).length;\n if (contradicting > 0) {\n const continuous = file.header.isBdf ? 'BDF+C' : 'EDF+C';\n const discontinuous = file.header.isBdf ? 'BDF+D' : 'EDF+D';\n diagnostics.push({\n code: 'DISCONTINUOUS',\n severity: 'warning',\n /*\n The markers as the file spells them.\n\n `continuity` normalises `BDF+C` to the internal `EDF+C` tag, and that tag reached\n the message: a BDF+ recording was told it is \"marked continuous (EDF+C)\" — a string\n it does not contain — and advised that it \"should have been marked EDF+D\", which is\n not a value BDF+ defines. A reader grepping the header for either finds nothing.\n\n The sibling discontinuous warning has done this since 0.3.x: `${'$'}{isBdf ? 'BDF+D' :\n 'EDF+D'}`. Same code, same header field, and the continuous branch never got it.\n */\n /*\n Both counts grouped, which is the whole point of the sentence: it puts one against\n the other. 0.8.5 gave `counted` the grouping and went through the bare numbers\n standing beside it — including this exact phrase in the ANNOTATION_DECODE_FAILED\n warning eighty lines down, `${grouped(missing.length)} of ${counted(...)}` — and\n missed this one, so a three-thousand-record file read \"2999 of its 3,000 data\n records\", three words apart.\n */\n message:\n `This file is marked continuous (${continuous}), but ${grouped(contradicting)} of its ` +\n `${counted(file.recordCount, 'data record')} ` +\n `${contradicting === 1 ? 'says it starts' : 'say they start'} somewhere other than ` +\n `where continuity puts ${contradicting === 1 ? 'it' : 'them'}.`,\n hint:\n `Times are written as if the records were contiguous, which is what ${continuous} ` +\n `means. If the recording really has gaps, the file should have been marked ` +\n `${discontinuous}.`,\n });\n }\n const first = origin;\n const last = contiguous[file.recordCount - 1] ?? first;\n if (!canCarry(last, file)) {\n diagnostics.push(unusableOrigin(first, file));\n return { starts: null, diagnostics };\n }\n /*\n An origin of zero is the same as no origin, for timing. It is not the same for the\n check above.\n\n This returned early on `origin === 0`, which is right about the times — contiguous\n starts from zero are what timing from zero already produces — and skipped the\n contradiction check on the way past. So an EDF+C file whose records say 0, 5 and 10 on\n one-second records went unreported, while the same file shifted one second, saying 1, 6\n and 11, was reported. The contradiction is in records 1 and 2 either way; where record 0\n happens to sit decides nothing about it.\n */\n if (origin === 0) return { starts: null, diagnostics };\n return { starts: contiguous, diagnostics };\n }\n\n if (file.annotationSignals.length === 0) {\n diagnostics.push({\n code: 'DISCONTINUOUS',\n severity: 'warning',\n message:\n 'This file is marked discontinuous but has no annotation channel, so where its ' +\n 'records sit in time is not recorded anywhere.',\n hint: 'Times are written as if the records were contiguous. Any gaps are lost.',\n });\n return { starts: null, diagnostics };\n }\n\n /*\n A record with no readable time is placed from the origin the other records establish,\n not from zero.\n\n `i * recordDuration` assumed the recording began at zero, which is the one thing the\n other records are in a position to contradict: a file starting at 0.5s put its\n unreadable record at 0.000 while its neighbours sat at 1.5s and 2.5s. The guess is still\n a guess — a discontinuous file may have a gap exactly there — and it is still reported\n below, but starting it from where the recording actually begins is strictly closer, and\n it makes an EDF+D file agree with its byte-identical EDF+C twin about record 0.\n */\n const base = originOf(annotationData.recordStarts, file.header.recordDuration) ?? 0;\n const starts = new Float64Array(file.recordCount);\n const missing: number[] = [];\n for (let i = 0; i < file.recordCount; i++) {\n const declared = annotationData.recordStarts[i];\n if (declared === null || declared === undefined) {\n missing.push(i);\n starts[i] = base + i * file.header.recordDuration;\n } else {\n starts[i] = declared;\n }\n }\n\n if (missing.length > 0) {\n /*\n Cut by the function that cuts every other list in a sentence here.\n\n This one rolled its own: five items and a bare `…`, where `listed` shows eight and counts\n what it left — \"and 32 more\" on the rate warning, \"and 112 more\" on the leftover files.\n The count is the honest half, and 0.7.58 taught the shared one not to hide a single item\n behind a phrase longer than the item. None of that reached this message, because it was\n never asking. Two implementations of one job, differing in the limit and in what they say\n about the tail.\n */\n const shown = listed(missing.map(String));\n // \"1 of 3 data records carry ... their true position\" — the subject is the one, not the three.\n const one = missing.length === 1;\n diagnostics.push({\n code: 'ANNOTATION_DECODE_FAILED',\n severity: 'warning',\n message:\n `${grouped(missing.length)} of ${counted(file.recordCount, 'data record')} ${one ? 'carries' : 'carry'} no ` +\n `readable timekeeping annotation (record${one ? '' : 's'} ${shown}), so ` +\n `${one ? 'its' : 'their'} true position in time is unknown.`,\n hint: one\n ? 'That record is timed as if it were contiguous; treat its timestamp as unreliable.'\n : 'Those records are timed as if they were contiguous; treat their timestamps as unreliable.',\n });\n }\n\n /*\n Furthest from zero, in either direction.\n\n This took the signed maximum and seeded it with 0, so a recording whose records all sit\n at negative onsets never got past the seed: `furthest` stayed 0, which any interval can\n carry. Then the samples collapsed anyway, because the arithmetic that defeats a large\n positive origin defeats a large negative one identically — at -1e16 seconds, adding a\n 1-second sample interval leaves the double unchanged.\n\n A four-record recording of eight samples wrote two rows, exit 0, no warning. Its\n byte-for-byte positive mirror wrote all eight and explained why it had to time them from\n zero. Same file, same failure, opposite sign, opposite outcome — and the silent one is\n the one that loses data, which is exactly what unusableOrigin exists to prevent.\n */\n let furthest = 0;\n for (const start of starts) if (Math.abs(start) > Math.abs(furthest)) furthest = start;\n if (!canCarry(furthest, file)) {\n diagnostics.push(unusableOrigin(furthest, file));\n return { starts: null, diagnostics };\n }\n\n /*\n Two ways a record can put the time column out of order, and only one was being looked for.\n\n A record starting before the one before it is the obvious case. The other is a record\n starting before the one before it *ends*: starts of 0, 0.5 and 1.0 on one-second records\n are strictly increasing, so nothing fired, and the rows still came out 0.25, 0.5, 0.75,\n 0.5 — because record 0's samples run to 0.75 while record 1 begins at 0.5. Overlapping\n acquisition is what a device does when it re-sends a buffer, and the reader has no more\n to say about it than about the reversed case: every sample is written, in file order,\n with the time the file gives it.\n\n Contiguity is not overlap. A continuous recording has `starts[i] === starts[i-1] +\n duration` exactly, so the comparison is made strict by a fraction of the finest interval\n the recording can express — the same measure the origin check uses.\n */\n const slack = finestInterval(file) / 2;\n let outOfOrder = 0;\n let overlapping = 0;\n for (let i = 1; i < starts.length; i++) {\n const previous = starts[i - 1] as number;\n const current = starts[i] as number;\n if (current < previous) outOfOrder++;\n else if (current + slack < previous + file.header.recordDuration) overlapping++;\n }\n if (outOfOrder > 0) {\n diagnostics.push({\n code: 'DISCONTINUOUS',\n severity: 'warning',\n message:\n `${counted(outOfOrder, 'data record')} ` +\n `${outOfOrder === 1 ? 'starts' : 'start'} earlier than the record before ` +\n `${outOfOrder === 1 ? 'it' : 'them'}.`,\n hint: 'Rows are written in file order, so the time column will not increase monotonically.',\n });\n }\n if (overlapping > 0) {\n diagnostics.push({\n code: 'DISCONTINUOUS',\n severity: 'warning',\n message:\n `${counted(overlapping, 'data record')} ` +\n `${overlapping === 1 ? 'starts' : 'start'} before the record before ` +\n `${overlapping === 1 ? 'it' : 'them'} ends, so ${overlapping === 1 ? 'its' : 'their'} ` +\n `samples overlap in time.`,\n hint: 'Rows are written in file order, so the time column will not increase monotonically.',\n });\n }\n\n return { starts, diagnostics };\n}\n\n/**\n * The recording's origin, from the first record that states where it is.\n *\n * Records of a continuous recording sit end to end, so record `i` beginning at `t` puts the\n * origin at `t - i * duration`. Any one readable timekeeping TAL is therefore enough, which\n * is what stops one unreadable entry from costing the whole file its position in time.\n */\nfunction originOf(recordStarts: readonly (number | null)[], recordDuration: number): number | null {\n for (const [index, declared] of recordStarts.entries()) {\n if (typeof declared !== 'number') continue;\n const origin = declared - index * recordDuration;\n return Number.isFinite(origin) ? origin : null;\n }\n return null;\n}\n\n/**\n * Whether times this far out can still tell one sample from the next.\n *\n * A double spaces its values further apart the larger they get: at 1e16 the gap is 2\n * seconds, so `t + 1` is `t`. Past that point a recording's declared origin stops being a\n * position and becomes a wall — the arithmetic that places records and samples returns the\n * origin itself, whatever is added to it.\n *\n * The finest thing that has to survive is the gap between two consecutive samples of the\n * fastest channel, since that is what the time column is made of. If that survives, so does\n * a whole record.\n */\nfunction canCarry(origin: number, file: EdfFile): boolean {\n if (!Number.isFinite(origin)) return false;\n // Asked of the origin furthest from zero, whichever side it is on: the spacing of doubles\n // grows with magnitude, not with value, so -1e16 and +1e16 fail this identically.\n return origin + finestInterval(file) > origin;\n}\n\n/**\n * The shortest span this recording can tell apart: one sample of its fastest channel.\n *\n * The time column is made of these, so nothing below one is a distinction the file is in a\n * position to make — which is what makes it the right size for both the \"can this origin\n * still separate two samples\" question and the \"is this record really somewhere else\"\n * question.\n */\nfunction finestInterval(file: EdfFile): number {\n let interval = file.header.recordDuration;\n for (const signal of file.header.signals) {\n if (signal.isAnnotations || !(signal.samplesPerRecord > 0)) continue;\n const step = file.header.recordDuration / signal.samplesPerRecord;\n if (step > 0 && step < interval) interval = step;\n }\n return interval;\n}\n\n/**\n * An origin the file's own arithmetic cannot express, reported rather than acted on.\n *\n * Two things went wrong when this was taken at face value, both of them quiet. A file whose\n * records all collapsed onto one instant made the recording zero seconds long, and the\n * window resolver — which had no reason to suspect the recording rather than the request —\n * blamed a flag nobody had passed:\n *\n * error: --start 100000000000000000s is at or past the end of this\n * 100000000000000000s recording.\n *\n * Slightly below that, the collapse is partial: `records[i].start + recordDuration` equals\n * the start again, so the test for \"does this record overlap the window\" fails for every\n * record whose neighbour rounded onto it. A twelve-row recording wrote four rows, exit 0,\n * no warning — the eight that vanished looked exactly like a file that never had them.\n *\n * Timing from zero is what the file did before 0.4.9 taught it to honour the first\n * timekeeping TAL, and at this magnitude it is the only column that can hold distinct\n * values. The origin is lost, so this says so.\n */\nfunction unusableOrigin(origin: number, file: EdfFile): Diagnostic {\n /*\n `Infinity` is a token, not a distance.\n\n `plain` expands exponent notation and hands anything else back, so a non-finite origin\n arrived on screen as \"place it Infinitys from its own start date\" — the exact form\n `formatDuration` exists to keep off the screen, its comment naming \"NaNs\" and \"Infinitys\"\n as what the fallback used to print, and the word the rest of this tool uses for the same\n quantity is \"unknown\". The origin overflows for the reason the recording's length does:\n a record duration near the top of a double, multiplied out over three records, leaves it.\n */\n const away = Number.isFinite(origin)\n ? `${plain(origin)}s from its own start date`\n : 'further from its own start date than a number can hold';\n return {\n code: 'DISCONTINUOUS',\n severity: 'warning',\n message:\n `This recording's timekeeping annotations place it ${away}, which is too far out for ` +\n `its ${plain(file.header.recordDuration)}s records to be told apart: at that magnitude ` +\n `adding a sample interval leaves the number unchanged.`,\n hint:\n 'Sample times are written from zero instead, so every row is present and the column ' +\n 'increases. Add the onsets in annotations.csv to recover absolute times if you need them.',\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"timing.js","sourceRoot":"","sources":["../../src/convert/timing.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Diagnostic } from '../edf/errors.js';\nimport type { EdfFile } from '../edf/reader.js';\nimport { counted, grouped, listed } from '../format/list.js';\nimport { plainSeconds } from '../format/number.js';\n\nexport interface AnnotationTimingData {\n recordStarts: (number | null)[];\n malformed: number;\n /** Unreadable TALs in first position, which carry timing rather than an event. */\n malformedTimekeeping?: number;\n malformedTimekeepingWithText?: number;\n}\n\n/**\n * The EDF+D warning's promise, withdrawn when the file cannot keep it.\n *\n * The header parser raises DISCONTINUOUS with the hint \"Each row carries its true recording\n * time, so gaps stay visible instead of being closed\" — which is what an EDF+D conversion\n * does, when the record times can be read. When they cannot, the very next warning in the\n * same run says the opposite: \"Times are written as if the records were contiguous. Any gaps\n * are lost.\" Two warnings, printed together, and the second denies the first.\n *\n * The parser cannot know: whether the starts can be derived is settled here, after the\n * annotation channel has been read. So the hint is amended where the answer is, the same way\n * `withoutFileRateWarning` drops a header diagnostic the plan has superseded.\n */\nexport function withTimingPromiseKept(\n diagnostics: readonly Diagnostic[],\n derived: boolean,\n): Diagnostic[] {\n if (derived) return [...diagnostics];\n return diagnostics.map((d) =>\n d.code === 'DISCONTINUOUS' && d.hint?.includes('gaps stay visible')\n ? {\n ...d,\n hint:\n 'Where its records sit in time is not recorded in this file, so they are written ' +\n 'as if contiguous — see the warning below.',\n }\n : d,\n );\n}\n\n/**\n * Resolve the true start time of every data record.\n *\n * Continuous recordings need no table because their record positions are\n * arithmetic. EDF+D recordings carry their positions in the annotation channel;\n * missing or malformed timekeeping entries are reported before falling back.\n */\nexport function deriveRecordStarts(\n file: EdfFile,\n annotationData: AnnotationTimingData,\n): { starts: Float64Array | null; diagnostics: Diagnostic[] } {\n const diagnostics: Diagnostic[] = [];\n\n if (annotationData.malformed > 0) {\n diagnostics.push({\n code: 'ANNOTATION_DECODE_FAILED',\n severity: 'warning',\n // Through `counted`, with the irregular plural spelled out — which is the case its\n // own comment names and which nothing was using it for.\n message:\n `${counted(annotationData.malformed, 'annotation entry', 'annotation entries')} ` +\n `${annotationData.malformed === 1 ? 'was' : 'were'} unreadable and could not be exported.`,\n hint: 'The rest were exported normally. The file may have been written by a non-conforming tool.',\n });\n }\n\n\n /*\n A timekeeping TAL is not an event, and saying it \"could not be exported\" describes the\n wrong loss twice over.\n\n These were counted among the annotations, so a file with one unreadable timekeeping TAL\n and three good events announced \"1 annotation entry was unreadable and could not be\n exported\" — while exporting all three. Nothing was missing from annotations.csv; what\n went missing was a record's position in time, which the message never mentioned.\n */\n const lostTimekeeping = annotationData.malformedTimekeeping ?? 0;\n const withText = annotationData.malformedTimekeepingWithText ?? 0;\n // The EDF+D branch below raises its own, which names the records and is more specific.\n // Saying both would report one problem twice.\n if (lostTimekeeping > 0 && file.header.continuity !== 'EDF+D') {\n const one = lostTimekeeping === 1;\n diagnostics.push({\n code: 'ANNOTATION_DECODE_FAILED',\n severity: 'warning',\n // Through `counted`, like the four other counts of \"data record\" in this file. This one\n // spelled the noun and the verb by hand — `record${one ? '' : 's'} carr${one ? 'ies' :\n // 'y'}` — while its nearest neighbour, sixty lines down and about the same records,\n // writes `${counted(n, 'data record')} ${one ? 'carries' : 'carry'}`. Same phrase, same\n // file, two spellings, and only one of them can be checked against the helper.\n message:\n `${counted(lostTimekeeping, 'data record')} ${one ? 'carries' : 'carry'} a timekeeping ` +\n `annotation that could not be read, so ${one ? 'it does' : 'they do'} not say where in ` +\n `time ${one ? 'it sits' : 'they sit'}.`,\n hint:\n /*\n \"No event was lost\" was said whatever the entry held.\n\n A first-position TAL states the record's start time and may carry events after it,\n which the specification allows and writers do. When one of those cannot be parsed\n the events go with it: a six-event file came out with two, and the only warning\n about it said in so many words that nothing had gone. Those entries are counted\n among the unreadable ones now, which is the warning printed above this one; this\n sentence keeps to what is true of the entries it is actually about.\n */\n /*\n Grouped, like the count of the same records one line above it.\n\n The message says `counted(lostTimekeeping, 'data record')`; the hint under it counts\n a subset of those records and spelled the number by hand. When the subset is all of\n them — which is the ordinary case, since a writer that cannot state an onset tends\n not to manage it anywhere — the two print the same number two lines apart, one\n grouped and one not:\n\n warning: 1,010 data records carry a timekeeping annotation that could not be\n read, so they do not say where in time they sit.\n 1010 of them also carried event text, which went with them and is\n counted above.\n */\n (withText > 0\n ? `${withText === 1 ? 'One of them' : `${grouped(withText)} of them`} also carried event text, ` +\n `which went with ${withText === 1 ? 'it' : 'them'} and is counted above. A ` +\n `timekeeping annotation itself states a record's start time and is never exported. `\n : 'No event was lost — a timekeeping annotation states a record\\'s start time and ' +\n 'is never exported. ') + 'Times are derived from the records that could be read.',\n });\n }\n\n /*\n A continuous recording's records are contiguous, but the first one need not sit at zero.\n\n EDF+ puts the header's start time and every annotation onset on one origin, and says the\n first data record's timekeeping TAL \"always starts with +0.X\", stating the fraction of a\n second by which that record follows it. Ignoring that fraction timed the samples from 0\n while the events kept their true onsets, so the two ended up on origins half a second\n apart — an event at +0.75 in a 4 Hz recording whose first TAL reads +0.5 landed on sample\n 3 instead of sample 1. The same file marked EDF+D, byte-identical but for the reserved\n field, placed it correctly, which is what gives the omission away.\n\n Records stay contiguous, which is what continuous means: only the origin moves. A first\n TAL of +0 needs no table at all, and that is nearly every file.\n */\n if (file.header.continuity !== 'EDF+D') {\n if (file.header.continuity !== 'EDF+C') {\n /*\n An annotation channel the reserved field never claimed.\n\n Without an `EDF+C` or `EDF+D` marker this is a plain EDF file, so the origin is not\n applied and the samples are timed from zero. The annotation channel is found by label\n rather than by the marker, though, so its events are still read and exported — with\n the onsets the file gives them.\n\n On a file whose timekeeping says the records start at 1000s, that put signals.csv at\n 0.000 and the event at 1000.5 in annotations.csv: two files from one conversion, a\n thousand seconds apart, and nothing said so. output-files promises the opposite —\n \"`onset_s` is on the same clock as `time_s` in the signal files\".\n\n Reported rather than repaired. Which clock is right is not knowable from here: the\n marker says plain EDF and the annotation channel says otherwise, and picking one would\n move either the samples or the events by the origin on a guess.\n */\n const stated = annotationData.recordStarts.find((start) => start !== null) ?? null;\n const base = file.header.isBdf ? 'BDF' : 'EDF';\n const plus = `${base}+`;\n if (stated !== null && Math.abs(stated) > 0) {\n diagnostics.push({\n code: 'MISSING_EDF_PLUS_MARKER',\n severity: 'warning',\n // The markers as this file spells them, which is what the contradiction warning\n // sixty lines down already does and what NO_SIGNAL_CHANNELS was fixed for. A BDF\n // recording was told to \"Mark the file EDF+C\" — not a value BDF+ defines — and\n // that it \"is read as plain EDF\", which it is not.\n message:\n `This file has an annotation channel stating that its records begin at ` +\n `${plainSeconds(stated)}s, but its reserved field carries no ${plus}C or ${plus}D ` +\n `marker — so it is read as plain ${base}, time_s counts from zero, and the two ` +\n `disagree by ${plainSeconds(stated)}s.`,\n hint:\n `annotations.csv keeps the onsets the file gives, so its events and signals.csv ` +\n `are on different clocks. Mark the file ${plus}C, or subtract the offset from ` +\n `the onsets, before joining them.`,\n });\n }\n return { starts: null, diagnostics };\n }\n\n /*\n The origin comes from whichever record first states one, not from record 0 alone.\n\n Reading only `recordStarts[0]` meant a single unreadable timekeeping TAL threw the\n origin away and timed the whole file from zero — while records 1 and 2, saying plainly\n that they start at 1.5s and 2.5s, went unread. A recording whose records sit at 0.5s,\n 1.5s and 2.5s came out with every sample 0.5s earlier than the file states, against\n annotation onsets that kept their true values. That is precisely the mismatch 0.4.9\n fixed, arriving through the one hole left in it, and the byte-identical EDF+D twin\n timed it correctly, which is what gives it away.\n\n Continuity is what makes this recoverable: record i sits at `origin + i * duration`,\n so any readable record determines the origin for all of them.\n */\n const origin = originOf(annotationData.recordStarts, file.header.recordDuration);\n if (origin === null) return { starts: null, diagnostics };\n\n const contiguous = new Float64Array(file.recordCount);\n for (let i = 0; i < file.recordCount; i++) {\n contiguous[i] = origin + i * file.header.recordDuration;\n }\n\n /*\n A file marked continuous whose own records disagree about it.\n\n Nothing looked at records past the first, so an EDF+C file whose records are in fact\n spread out was timed as though they were contiguous and said nothing. The records are\n being read here anyway, so the contradiction costs nothing to notice — and it is the\n file, not the reader, that has to be wrong for this to fire.\n\n Compared against what the file can express, not for equality. 0.4.41 asked whether the\n two doubles were the same, which they are not: a recording of 0.1s records sitting at\n 0.1, 0.2, 0.3 ... is contiguous by construction, and 0.1 + 2 * 0.1 is\n 0.30000000000000004. Two of its eight records were reported as contradicting\n continuity, on an ordinary file — and under --strict that was a failed run. The\n smallest interval the recording distinguishes is one sample of its fastest channel;\n anything below half of that is arithmetic, not a gap. `canCarry` has already refused\n origins where the double spacing swamps that interval, so the representation error is\n under the tolerance by construction rather than by hope.\n */\n const tolerance = finestInterval(file) / 2;\n const contradicting = annotationData.recordStarts.filter(\n (declared, i) =>\n typeof declared === 'number' && Math.abs(declared - (contiguous[i] as number)) > tolerance,\n ).length;\n if (contradicting > 0) {\n const continuous = file.header.isBdf ? 'BDF+C' : 'EDF+C';\n const discontinuous = file.header.isBdf ? 'BDF+D' : 'EDF+D';\n diagnostics.push({\n code: 'DISCONTINUOUS',\n severity: 'warning',\n /*\n The markers as the file spells them.\n\n `continuity` normalises `BDF+C` to the internal `EDF+C` tag, and that tag reached\n the message: a BDF+ recording was told it is \"marked continuous (EDF+C)\" — a string\n it does not contain — and advised that it \"should have been marked EDF+D\", which is\n not a value BDF+ defines. A reader grepping the header for either finds nothing.\n\n The sibling discontinuous warning has done this since 0.3.x: `${'$'}{isBdf ? 'BDF+D' :\n 'EDF+D'}`. Same code, same header field, and the continuous branch never got it.\n */\n /*\n Both counts grouped, which is the whole point of the sentence: it puts one against\n the other. 0.8.5 gave `counted` the grouping and went through the bare numbers\n standing beside it — including this exact phrase in the ANNOTATION_DECODE_FAILED\n warning eighty lines down, `${grouped(missing.length)} of ${counted(...)}` — and\n missed this one, so a three-thousand-record file read \"2999 of its 3,000 data\n records\", three words apart.\n */\n message:\n `This file is marked continuous (${continuous}), but ${grouped(contradicting)} of its ` +\n `${counted(file.recordCount, 'data record')} ` +\n `${contradicting === 1 ? 'says it starts' : 'say they start'} somewhere other than ` +\n `where continuity puts ${contradicting === 1 ? 'it' : 'them'}.`,\n hint:\n `Times are written as if the records were contiguous, which is what ${continuous} ` +\n `means. If the recording really has gaps, the file should have been marked ` +\n `${discontinuous}.`,\n });\n }\n const first = origin;\n const last = contiguous[file.recordCount - 1] ?? first;\n if (!canCarry(last, file)) {\n diagnostics.push(unusableOrigin(first, file));\n return { starts: null, diagnostics };\n }\n /*\n An origin of zero is the same as no origin, for timing. It is not the same for the\n check above.\n\n This returned early on `origin === 0`, which is right about the times — contiguous\n starts from zero are what timing from zero already produces — and skipped the\n contradiction check on the way past. So an EDF+C file whose records say 0, 5 and 10 on\n one-second records went unreported, while the same file shifted one second, saying 1, 6\n and 11, was reported. The contradiction is in records 1 and 2 either way; where record 0\n happens to sit decides nothing about it.\n */\n if (origin === 0) return { starts: null, diagnostics };\n return { starts: contiguous, diagnostics };\n }\n\n if (file.annotationSignals.length === 0) {\n diagnostics.push({\n code: 'DISCONTINUOUS',\n severity: 'warning',\n message:\n 'This file is marked discontinuous but has no annotation channel, so where its ' +\n 'records sit in time is not recorded anywhere.',\n hint: 'Times are written as if the records were contiguous. Any gaps are lost.',\n });\n return { starts: null, diagnostics };\n }\n\n /*\n A record with no readable time is placed from the origin the other records establish,\n not from zero.\n\n `i * recordDuration` assumed the recording began at zero, which is the one thing the\n other records are in a position to contradict: a file starting at 0.5s put its\n unreadable record at 0.000 while its neighbours sat at 1.5s and 2.5s. The guess is still\n a guess — a discontinuous file may have a gap exactly there — and it is still reported\n below, but starting it from where the recording actually begins is strictly closer, and\n it makes an EDF+D file agree with its byte-identical EDF+C twin about record 0.\n */\n const base = originOf(annotationData.recordStarts, file.header.recordDuration) ?? 0;\n const starts = new Float64Array(file.recordCount);\n const missing: number[] = [];\n for (let i = 0; i < file.recordCount; i++) {\n const declared = annotationData.recordStarts[i];\n if (declared === null || declared === undefined) {\n missing.push(i);\n starts[i] = base + i * file.header.recordDuration;\n } else {\n starts[i] = declared;\n }\n }\n\n if (missing.length > 0) {\n /*\n Cut by the function that cuts every other list in a sentence here.\n\n This one rolled its own: five items and a bare `…`, where `listed` shows eight and counts\n what it left — \"and 32 more\" on the rate warning, \"and 112 more\" on the leftover files.\n The count is the honest half, and 0.7.58 taught the shared one not to hide a single item\n behind a phrase longer than the item. None of that reached this message, because it was\n never asking. Two implementations of one job, differing in the limit and in what they say\n about the tail.\n */\n const shown = listed(missing.map(String));\n // \"1 of 3 data records carry ... their true position\" — the subject is the one, not the three.\n const one = missing.length === 1;\n diagnostics.push({\n code: 'ANNOTATION_DECODE_FAILED',\n severity: 'warning',\n message:\n `${grouped(missing.length)} of ${counted(file.recordCount, 'data record')} ${one ? 'carries' : 'carry'} no ` +\n `readable timekeeping annotation (record${one ? '' : 's'} ${shown}), so ` +\n `${one ? 'its' : 'their'} true position in time is unknown.`,\n hint: one\n ? 'That record is timed as if it were contiguous; treat its timestamp as unreliable.'\n : 'Those records are timed as if they were contiguous; treat their timestamps as unreliable.',\n });\n }\n\n /*\n Furthest from zero, in either direction.\n\n This took the signed maximum and seeded it with 0, so a recording whose records all sit\n at negative onsets never got past the seed: `furthest` stayed 0, which any interval can\n carry. Then the samples collapsed anyway, because the arithmetic that defeats a large\n positive origin defeats a large negative one identically — at -1e16 seconds, adding a\n 1-second sample interval leaves the double unchanged.\n\n A four-record recording of eight samples wrote two rows, exit 0, no warning. Its\n byte-for-byte positive mirror wrote all eight and explained why it had to time them from\n zero. Same file, same failure, opposite sign, opposite outcome — and the silent one is\n the one that loses data, which is exactly what unusableOrigin exists to prevent.\n */\n let furthest = 0;\n for (const start of starts) if (Math.abs(start) > Math.abs(furthest)) furthest = start;\n if (!canCarry(furthest, file)) {\n diagnostics.push(unusableOrigin(furthest, file));\n return { starts: null, diagnostics };\n }\n\n /*\n Two ways a record can put the time column out of order, and only one was being looked for.\n\n A record starting before the one before it is the obvious case. The other is a record\n starting before the one before it *ends*: starts of 0, 0.5 and 1.0 on one-second records\n are strictly increasing, so nothing fired, and the rows still came out 0.25, 0.5, 0.75,\n 0.5 — because record 0's samples run to 0.75 while record 1 begins at 0.5. Overlapping\n acquisition is what a device does when it re-sends a buffer, and the reader has no more\n to say about it than about the reversed case: every sample is written, in file order,\n with the time the file gives it.\n\n Contiguity is not overlap. A continuous recording has `starts[i] === starts[i-1] +\n duration` exactly, so the comparison is made strict by a fraction of the finest interval\n the recording can express — the same measure the origin check uses.\n */\n const slack = finestInterval(file) / 2;\n let outOfOrder = 0;\n let overlapping = 0;\n for (let i = 1; i < starts.length; i++) {\n const previous = starts[i - 1] as number;\n const current = starts[i] as number;\n if (current < previous) outOfOrder++;\n else if (current + slack < previous + file.header.recordDuration) overlapping++;\n }\n if (outOfOrder > 0) {\n diagnostics.push({\n code: 'DISCONTINUOUS',\n severity: 'warning',\n message:\n `${counted(outOfOrder, 'data record')} ` +\n `${outOfOrder === 1 ? 'starts' : 'start'} earlier than the record before ` +\n `${outOfOrder === 1 ? 'it' : 'them'}.`,\n hint: 'Rows are written in file order, so the time column will not increase monotonically.',\n });\n }\n if (overlapping > 0) {\n diagnostics.push({\n code: 'DISCONTINUOUS',\n severity: 'warning',\n message:\n `${counted(overlapping, 'data record')} ` +\n `${overlapping === 1 ? 'starts' : 'start'} before the record before ` +\n `${overlapping === 1 ? 'it' : 'them'} ends, so ${overlapping === 1 ? 'its' : 'their'} ` +\n `samples overlap in time.`,\n hint: 'Rows are written in file order, so the time column will not increase monotonically.',\n });\n }\n\n return { starts, diagnostics };\n}\n\n/**\n * The recording's origin, from the first record that states where it is.\n *\n * Records of a continuous recording sit end to end, so record `i` beginning at `t` puts the\n * origin at `t - i * duration`. Any one readable timekeeping TAL is therefore enough, which\n * is what stops one unreadable entry from costing the whole file its position in time.\n */\nfunction originOf(recordStarts: readonly (number | null)[], recordDuration: number): number | null {\n for (const [index, declared] of recordStarts.entries()) {\n if (typeof declared !== 'number') continue;\n const origin = declared - index * recordDuration;\n return Number.isFinite(origin) ? origin : null;\n }\n return null;\n}\n\n/**\n * Whether times this far out can still tell one sample from the next.\n *\n * A double spaces its values further apart the larger they get: at 1e16 the gap is 2\n * seconds, so `t + 1` is `t`. Past that point a recording's declared origin stops being a\n * position and becomes a wall — the arithmetic that places records and samples returns the\n * origin itself, whatever is added to it.\n *\n * The finest thing that has to survive is the gap between two consecutive samples of the\n * fastest channel, since that is what the time column is made of. If that survives, so does\n * a whole record.\n */\nfunction canCarry(origin: number, file: EdfFile): boolean {\n if (!Number.isFinite(origin)) return false;\n // Asked of the origin furthest from zero, whichever side it is on: the spacing of doubles\n // grows with magnitude, not with value, so -1e16 and +1e16 fail this identically.\n return origin + finestInterval(file) > origin;\n}\n\n/**\n * The shortest span this recording can tell apart: one sample of its fastest channel.\n *\n * The time column is made of these, so nothing below one is a distinction the file is in a\n * position to make — which is what makes it the right size for both the \"can this origin\n * still separate two samples\" question and the \"is this record really somewhere else\"\n * question.\n */\nfunction finestInterval(file: EdfFile): number {\n let interval = file.header.recordDuration;\n for (const signal of file.header.signals) {\n if (signal.isAnnotations || !(signal.samplesPerRecord > 0)) continue;\n const step = file.header.recordDuration / signal.samplesPerRecord;\n if (step > 0 && step < interval) interval = step;\n }\n return interval;\n}\n\n/**\n * An origin the file's own arithmetic cannot express, reported rather than acted on.\n *\n * Two things went wrong when this was taken at face value, both of them quiet. A file whose\n * records all collapsed onto one instant made the recording zero seconds long, and the\n * window resolver — which had no reason to suspect the recording rather than the request —\n * blamed a flag nobody had passed:\n *\n * error: --start 100000000000000000s is at or past the end of this\n * 100000000000000000s recording.\n *\n * Slightly below that, the collapse is partial: `records[i].start + recordDuration` equals\n * the start again, so the test for \"does this record overlap the window\" fails for every\n * record whose neighbour rounded onto it. A twelve-row recording wrote four rows, exit 0,\n * no warning — the eight that vanished looked exactly like a file that never had them.\n *\n * Timing from zero is what the file did before 0.4.9 taught it to honour the first\n * timekeeping TAL, and at this magnitude it is the only column that can hold distinct\n * values. The origin is lost, so this says so.\n */\nfunction unusableOrigin(origin: number, file: EdfFile): Diagnostic {\n /*\n `Infinity` is a token, not a distance.\n\n `plain` expands exponent notation and hands anything else back, so a non-finite origin\n arrived on screen as \"place it Infinitys from its own start date\" — the exact form\n `formatDuration` exists to keep off the screen, its comment naming \"NaNs\" and \"Infinitys\"\n as what the fallback used to print, and the word the rest of this tool uses for the same\n quantity is \"unknown\". The origin overflows for the reason the recording's length does:\n a record duration near the top of a double, multiplied out over three records, leaves it.\n */\n const away = Number.isFinite(origin)\n ? `${plainSeconds(origin)}s from its own start date`\n : 'further from its own start date than a number can hold';\n return {\n code: 'DISCONTINUOUS',\n severity: 'warning',\n message:\n `This recording's timekeeping annotations place it ${away}, which is too far out for ` +\n `its ${plainSeconds(file.header.recordDuration)}s records to be told apart: at that magnitude ` +\n `adding a sample interval leaves the number unchanged.`,\n hint:\n 'Sample times are written from zero instead, so every row is present and the column ' +\n 'increases. Add the onsets in annotations.csv to recover absolute times if you need them.',\n };\n}\n"]}
|
package/dist/edf/header.js
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
*/
|
|
31
31
|
import { EdfError } from './errors.js';
|
|
32
32
|
import { counted, grouped, listed } from '../format/list.js';
|
|
33
|
-
import {
|
|
33
|
+
import { plainSeconds } from '../format/number.js';
|
|
34
34
|
// The shell-quoting rule for a label, shared rather than repeated: this hint and
|
|
35
35
|
// `--channels`' own "did you mean" have to print the same command for the same label.
|
|
36
36
|
import { typeable } from '../convert/channels.js';
|
|
@@ -379,7 +379,7 @@ export function parseHeader(buf, fileSize) {
|
|
|
379
379
|
throw new EdfError('INVALID_SIGNAL_COUNT', `Header declares ${counted(signalCount, 'signal')}; expected at least 1.`, BAD_FIELD_HINT);
|
|
380
380
|
}
|
|
381
381
|
if (!(recordDuration > 0)) {
|
|
382
|
-
throw new EdfError('INVALID_RECORD_DURATION', `Header declares a data record duration of ${
|
|
382
|
+
throw new EdfError('INVALID_RECORD_DURATION', `Header declares a data record duration of ${plainSeconds(recordDuration)}s; expected a positive number.`, BAD_FIELD_HINT);
|
|
383
383
|
}
|
|
384
384
|
const expectedHeaderBytes = FIXED_HEADER_BYTES + signalCount * SIGNAL_HEADER_BYTES;
|
|
385
385
|
if (buf.length < expectedHeaderBytes) {
|