@fable-org/fable-library-ts 2.4.0 → 2.4.1

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/BigInt.ts CHANGED
@@ -135,12 +135,12 @@ export function toIntN_unchecked(bits: number, x: bigint, signed: boolean): bigi
135
135
  }
136
136
 
137
137
  export function toIntN(bits: number, x: bigint, signed: boolean): bigint {
138
- let higher_bits = abs(x) >> BigInt(bits);
139
- if (higher_bits !== 0n) {
138
+ const truncated = signed ? BigInt.asIntN(bits, x) : BigInt.asUintN(bits, x);
139
+ if (truncated !== x) {
140
140
  const s = signed ? "a signed" : "an unsigned";
141
141
  throw new Exception(`Value was either too large or too small for ${s} ${bits}-bit integer.`);
142
142
  }
143
- return signed ? BigInt.asIntN(bits, x) : BigInt.asUintN(bits, x);
143
+ return truncated;
144
144
  }
145
145
 
146
146
  export function toInt8(x: bigint): int8 { return Number(toIntN(8, x, true)); }
@@ -176,6 +176,9 @@ export function toFloat64(x: bigint): float64 { return Number(x); }
176
176
  export function toDecimal(x: bigint): decimal {
177
177
  const isNegative = x < zero;
178
178
  const bits = abs(x);
179
+ if ((bits >> 96n) !== zero) {
180
+ throw new Exception("Value was either too large or too small for a Decimal.");
181
+ }
179
182
  const low = Number(BigInt.asUintN(32, bits));
180
183
  const mid = Number(BigInt.asUintN(32, bits >> 32n));
181
184
  const high = Number(BigInt.asUintN(32, bits >> 64n));
@@ -228,6 +231,8 @@ export function divRem(x: bigint, y: bigint, out?: FSharpRef<bigint>): bigint |
228
231
  }
229
232
 
230
233
  export function greatestCommonDivisor(x: bigint, y: bigint): bigint {
234
+ x = abs(x);
235
+ y = abs(y);
231
236
  while (y > zero) {
232
237
  const q = x / y;
233
238
  const r = x - q * y;
@@ -237,8 +242,17 @@ export function greatestCommonDivisor(x: bigint, y: bigint): bigint {
237
242
  return x;
238
243
  }
239
244
 
245
+ // Number of bits in the binary representation of x (x must be positive)
246
+ function bitLength(x: bigint): number {
247
+ const hex = x.toString(16);
248
+ return (hex.length - 1) * 4 + (32 - Math.clz32(fromHexCode(hex.charCodeAt(0))));
249
+ }
250
+
240
251
  export function getBitLength(x: bigint): int64 {
241
- return fromFloat64(x === zero ? 1 : log2(abs(x)) + 1);
252
+ if (x < zero) {
253
+ x = -x - one; // two's complement bit length excluding the sign bit
254
+ }
255
+ return x === zero ? zero : BigInt(bitLength(x));
242
256
  }
243
257
 
244
258
  export function log2(x: bigint): float64 {
@@ -274,7 +288,10 @@ export function log(x: bigint, base: float64): float64 {
274
288
  }
275
289
 
276
290
  export function ilog2(x: bigint): bigint {
277
- return BigInt(log2(x));
291
+ if (x < zero) {
292
+ throw new Exception("Value must be non-negative.");
293
+ }
294
+ return x === zero ? zero : BigInt(bitLength(x) - 1);
278
295
  }
279
296
 
280
297
  // export function copySign
@@ -305,35 +322,20 @@ function fromHexCode(code: number): number {
305
322
 
306
323
  function toSignedBytes(x: bigint, isBigEndian: boolean): Uint8Array {
307
324
  const isNeg = x < 0n;
308
- if (isNeg) {
309
- const len = log2(-x);
310
- const bits = len + (8 - len % 8);
311
- const pow2 = (1n << BigInt(bits));
312
- x = x + pow2; // two's complement
313
- }
314
- const hex = x.toString(16);
315
- const len = hex.length;
316
- const odd = len % 2;
317
- const first = hex.charCodeAt(0);
318
- const isLow = 48 <= first && first <= 55; // 0..7
319
- const start = (isNeg && isLow) || (!isNeg && !isLow) ? 1 : 0;
320
- const bytes = new Uint8Array(start + (len + odd) / 2);
321
- const inc = isBigEndian ? 1 : -1;
322
- let pos = isBigEndian ? 0 : bytes.length - 1;
323
- if (start > 0) {
324
- bytes[pos] = isNeg ? 255 : 0;
325
- pos += inc;
326
- }
327
- if (odd > 0) {
328
- bytes[pos] = fromHexCode(first);
329
- pos += inc;
325
+ const sentinel = isNeg ? -1n : 0n;
326
+ const bytes: number[] = []; // little-endian
327
+ do {
328
+ bytes.push(Number(BigInt.asUintN(8, x)));
329
+ x = x >> 8n;
330
+ } while (x !== sentinel);
331
+ // extra byte if the top byte's sign bit would misrepresent the sign
332
+ if (isNeg !== (bytes[bytes.length - 1] > 127)) {
333
+ bytes.push(isNeg ? 255 : 0);
330
334
  }
331
- for (let i = odd; i < len; i += 2, pos += inc) {
332
- const a = fromHexCode(hex.charCodeAt(i));
333
- const b = fromHexCode(hex.charCodeAt(i + 1));
334
- bytes[pos] = (a << 4) | b;
335
+ if (isBigEndian) {
336
+ bytes.reverse();
335
337
  }
336
- return bytes;
338
+ return new Uint8Array(bytes);
337
339
  }
338
340
 
339
341
  function fromSignedBytes(bytes: ArrayLike<uint8>, isBigEndian: boolean) {
package/CHANGELOG.md CHANGED
@@ -1,5 +1,5 @@
1
1
  ---
2
- last_commit_released: 7f915f1dd66b9a5fbbd56f858b07d39b98519b65
2
+ last_commit_released: d6ae6bd3790b57b31941a118cdffaeb6a59155c3
3
3
  updaters:
4
4
  - package.json:
5
5
  file: package.json
@@ -15,6 +15,21 @@ All notable changes to this project will be documented in this file.
15
15
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
16
16
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
17
17
 
18
+ ## 2.4.1 - 2026-07-09
19
+
20
+ ### 🐞 Bug Fixes
21
+
22
+ * MailboxProcessor dropping falsy messages and unit replies ([8299b2e7](https://github.com/fable-compiler/Fable/commit/8299b2e7f690e9136906653155883a08607aa4a4))
23
+ * *(all)* Add `Seq.enumerateTryWith` for try/with in seq, list, array comprehensions (#4750) ([448c90d7](https://github.com/fable-compiler/Fable/commit/448c90d7ccba80ee3d3c38432a2a1d1407f6dc69))
24
+ * *(js/ts)* Match .NET NaN semantics in JS comparison, min and max ([7e5106f9](https://github.com/fable-compiler/Fable/commit/7e5106f9d93cb55ae4aca4656868fa0c53493991))
25
+ * *(js/ts)* BigInteger byte-array corruption, checked conversions, gcd, log2 (#4743) ([f3cb7f2b](https://github.com/fable-compiler/Fable/commit/f3cb7f2bfc9e46e558763867d6c1ba323bcf281c))
26
+ * *(js/ts)* Observable/Event dropping null/undefined/unit values (#4742) ([75bff73b](https://github.com/fable-compiler/Fable/commit/75bff73be3743fe0af37f99c541b10f6978150f3))
27
+ * *(js/ts)* EndsWith regression for empty pattern with non-ordinal comparison ([7793ed7b](https://github.com/fable-compiler/Fable/commit/7793ed7b0fc1f33bf3da6c0ea31607e172726267))
28
+ * *(js/ts/python)* Quote strings with the %A format specifier (#4749) ([75321f3b](https://github.com/fable-compiler/Fable/commit/75321f3b2cb70ea345a0a45d6343cbb6e0de466f))
29
+ * *(ts/js)* `Regex.Replace` count/substitutions and `Regex.Split` semantics (#4741) ([d6ae6bd3](https://github.com/fable-compiler/Fable/commit/d6ae6bd3790b57b31941a118cdffaeb6a59155c3))
30
+
31
+ <strong><small>[View changes on Github](https://github.com/fable-compiler/Fable/compare/7f915f1dd66b9a5fbbd56f858b07d39b98519b65..d6ae6bd3790b57b31941a118cdffaeb6a59155c3)</small></strong>
32
+
18
33
  ## 2.4.0 - 2026-07-05
19
34
 
20
35
  ### 🚀 Features
package/Double.ts CHANGED
@@ -36,18 +36,20 @@ export function isInfinity(x: number) {
36
36
  }
37
37
 
38
38
  export function max(x: number, y: number): number {
39
- return x > y ? x : y;
39
+ return Math.max(x, y);
40
40
  }
41
41
 
42
42
  export function min(x: number, y: number): number {
43
- return x < y ? x : y;
43
+ return Math.min(x, y);
44
44
  }
45
45
 
46
46
  export function maxMagnitude(x: number, y: number): number {
47
+ if (Number.isNaN(x) || Number.isNaN(y)) { return NaN; }
47
48
  return Math.abs(x) > Math.abs(y) ? x : y;
48
49
  }
49
50
 
50
51
  export function minMagnitude(x: number, y: number): number {
52
+ if (Number.isNaN(x) || Number.isNaN(y)) { return NaN; }
51
53
  return Math.abs(x) < Math.abs(y) ? x : y;
52
54
  }
53
55
 
package/Event.ts CHANGED
@@ -35,8 +35,16 @@ export class Event$2<Delegate extends Function, Args> {
35
35
  public Trigger(value: Args): void;
36
36
  public Trigger(sender: any, value: Args): void
37
37
  public Trigger(senderOrValue: any, valueOrUndefined?: Args): void {
38
+ // Disambiguate the overloads by arity, not by value: a two-argument call may
39
+ // legitimately pass `undefined` as the event value (e.g. unit events).
38
40
  let sender: any = null;
39
- const value = valueOrUndefined === undefined ? senderOrValue as Args : (sender = senderOrValue, valueOrUndefined);
41
+ let value: Args;
42
+ if (arguments.length <= 1) {
43
+ value = senderOrValue as Args;
44
+ } else {
45
+ sender = senderOrValue;
46
+ value = valueOrUndefined as Args;
47
+ }
40
48
  this.delegates.forEach(f => { f(sender, value) });
41
49
  }
42
50
  }
package/List.ts CHANGED
@@ -1,9 +1,8 @@
1
1
 
2
- import { join } from "./String.ts";
2
+ import { IComparer, IEqualityComparer, isArrayLike, MutableArray, Exception, defaultOf, toIterator, compare, structuralHash, equals, IDisposable, disposeSafe, IEnumerator, getEnumerator } from "./Util.ts";
3
+ import { Record, toString } from "./Types.ts";
3
4
  import { defaultArg, some, value as value_1, Option } from "./Option.ts";
4
- import { IComparer, IEqualityComparer, disposeSafe, isArrayLike, MutableArray, Exception, IDisposable, defaultOf, toIterator, getEnumerator, IEnumerator, compare, structuralHash, equals } from "./Util.ts";
5
5
  import { float64, int32 } from "./Int32.ts";
6
- import { Record } from "./Types.ts";
7
6
  import { class_type, record_type, option_type, TypeInfo } from "./Reflection.ts";
8
7
  import { SR_inputSequenceTooLong, SR_inputSequenceEmpty, SR_inputMustBeNonNegative, SR_notEnoughElements, SR_differentLengths, SR_keyNotFoundAlt, SR_indexOutOfBounds, SR_inputWasEmpty } from "./Global.ts";
9
8
  import { KeyNotFoundException_$ctor_Z721C83C5 } from "./System.Collections.Generic.ts";
@@ -20,7 +19,21 @@ export class FSharpList<T> extends Record implements Iterable<T> {
20
19
  }
21
20
  toString(): string {
22
21
  const xs: FSharpList<T> = this;
23
- return ("[" + join("; ", xs)) + "]";
22
+ let result = "[";
23
+ let first = true;
24
+ const enumerator: IEnumerator<T> = getEnumerator(xs);
25
+ try {
26
+ while (enumerator["System.Collections.IEnumerator.MoveNext"]()) {
27
+ let x: T = (undefined as any), matchValue: any = (undefined as any), s: string = (undefined as any);
28
+ const x_1: T = enumerator["System.Collections.Generic.IEnumerator`1.get_Current"]();
29
+ result = ((first ? result : (result + "; ")) + ((x = x_1, (matchValue = x, (typeof matchValue === "string") ? ((s = (matchValue as string), ("\"" + s) + "\"")) : toString(x)))));
30
+ first = false;
31
+ }
32
+ }
33
+ finally {
34
+ disposeSafe(enumerator as IDisposable);
35
+ }
36
+ return result + "]";
24
37
  }
25
38
  Equals(other: any): boolean {
26
39
  const xs: FSharpList<T> = this;
@@ -28,7 +28,7 @@ class MailboxQueue<Msg> {
28
28
  }
29
29
  }
30
30
 
31
- public tryGet() {
31
+ public tryGet(): { value: Msg } | undefined {
32
32
  if (this.firstAndLast) {
33
33
  const value = this.firstAndLast[0].value;
34
34
  if (this.firstAndLast[0].next) {
@@ -36,7 +36,9 @@ class MailboxQueue<Msg> {
36
36
  } else {
37
37
  delete this.firstAndLast;
38
38
  }
39
- return value;
39
+ // Wrap the value so falsy messages (0, false, "", null, undefined/unit)
40
+ // can be distinguished from an empty queue.
41
+ return { value };
40
42
  }
41
43
  return void 0;
42
44
  }
@@ -64,11 +66,11 @@ export class MailboxProcessor<Msg> {
64
66
 
65
67
  function __processEvents<Msg>($this: MailboxProcessor<Msg>) {
66
68
  if ($this.continuation) {
67
- const value = $this.messages.tryGet();
68
- if (value) {
69
+ const dequeued = $this.messages.tryGet();
70
+ if (dequeued !== void 0) {
69
71
  const cont = $this.continuation;
70
72
  delete $this.continuation;
71
- cont(value);
73
+ cont(dequeued.value);
72
74
  }
73
75
  }
74
76
  }
@@ -97,15 +99,19 @@ export function postAndAsyncReply<Reply, Msg>(
97
99
  buildMessage: (c: AsyncReplyChannel<Reply>) => Msg,
98
100
  ) {
99
101
  let result: Reply;
102
+ // Use an explicit flag because the reply value itself may be undefined
103
+ // (e.g. AsyncReplyChannel<unit>), which must still complete the async.
104
+ let replied = false;
100
105
  let continuation: Continuation<Reply>;
101
106
  function checkCompletion() {
102
- if (result !== void 0 && continuation !== void 0) {
107
+ if (replied && continuation !== void 0) {
103
108
  continuation(result);
104
109
  }
105
110
  }
106
111
  const reply = {
107
112
  reply: (res: Reply) => {
108
113
  result = res;
114
+ replied = true;
109
115
  checkCompletion();
110
116
  },
111
117
  };
package/Observable.ts CHANGED
@@ -1,6 +1,6 @@
1
1
 
2
2
  import { FSharpChoice$2_$union, Choice_tryValueIfChoice1Of2, Choice_tryValueIfChoice2Of2 } from "./Choice.ts";
3
- import { Option, value } from "./Option.ts";
3
+ import { Option, some, value } from "./Option.ts";
4
4
  import { IDisposable } from "./Util.ts";
5
5
 
6
6
  export interface IObserver<T> {
@@ -56,7 +56,7 @@ export function choose<T, U>(chooser: (x: T) => Option<U>, source: IObservable<T
56
56
  }
57
57
 
58
58
  export function filter<T>(predicate: (x: T) => boolean, source: IObservable<T>): IObservable<T> {
59
- return choose((x) => predicate(x) ? x : void 0, source);
59
+ return choose((x) => predicate(x) ? some(x) : undefined, source);
60
60
  }
61
61
 
62
62
  export function map<T, U>(mapping: (x: T) => U, source: IObservable<T>): IObservable<U> {
@@ -120,11 +120,13 @@ export function merge<T>(source1: IObservable<T>, source2: IObservable<T>): IObs
120
120
  export function pairwise<T>(source: IObservable<T>): IObservable<[T, T]> {
121
121
  return new Observable<[T, T]>((observer) => {
122
122
  let last: T;
123
+ let haveLast = false;
123
124
  return source.Subscribe(new Observer<T>((next) => {
124
- if (last != null) {
125
+ if (haveLast) {
125
126
  observer.OnNext([last, next]);
126
127
  }
127
128
  last = next;
129
+ haveLast = true;
128
130
  }, observer.OnError, observer.OnCompleted));
129
131
  });
130
132
  }
package/RegExp.ts CHANGED
@@ -117,28 +117,32 @@ export function replace(
117
117
  input = tmp;
118
118
  limit = undefined;
119
119
  }
120
- if (typeof replacement === "function") {
121
- limit = limit == null ? -1 : limit;
122
- return input.substring(0, offset) + input.substring(offset).replace(reg as RegExp, replacer);
123
- } else {
124
- replacement =
125
- replacement
126
- // $0 doesn't work with JS regex, see #1155
127
- .replace(/\$0/g, (_s) => "$&")
128
- // named groups in replacement are `${name}` in .Net, but `$<name>` in JS (in regex: groups are `(?<name>...)` in both)
129
- .replace(/\${([^}]+)}/g, "\$<$1>")
130
- ;
131
- if (limit != null) {
132
- let m: RegExpExecArray;
133
- const sub1 = input.substring(offset);
134
- const _matches = matches(reg, sub1);
135
- const sub2 = matches.length > limit ? (m = _matches[limit - 1], sub1.substring(0, m.index + m[0].length)) : sub1;
136
- return input.substring(0, offset) + sub2.replace(reg as RegExp, replacement as string)
137
- + input.substring(offset + sub2.length);
138
- } else {
139
- return input.replace(reg as RegExp, replacement as string);
140
- }
120
+ if (typeof replacement === "string") {
121
+ const rep = replacement as string;
122
+ // .NET replacement patterns cannot be rewritten as JS replacement strings unambiguously
123
+ // (e.g. `$$0` must stay literal "$0", `$0` doesn't work with JS regex (see #1155), and
124
+ // `${1}` followed by a digit must not become `$1` + digit), so interpret them with an
125
+ // evaluator instead, honoring the `$$` escape left-to-right
126
+ replacement = (match: any): string =>
127
+ rep.replace(/\$(?:(\$)|(&)|(`)|(')|(\d+)|\{(\d+)\}|\{([^}]+)\})/g,
128
+ (s, dollar?: string, and?: string, before?: string, after?: string, num?: string, numBraced?: string, name?: string) => {
129
+ if (dollar != null) { return "$"; }
130
+ if (and != null) { return match[0]; }
131
+ if (before != null) { return match.input.substring(0, match.index); }
132
+ if (after != null) { return match.input.substring(match.index + match[0].length); }
133
+ const n = num ?? numBraced;
134
+ if (n != null) {
135
+ const i = parseInt(n, 10);
136
+ // Same as .NET: an unmatched group is replaced by an empty string,
137
+ // a nonexistent group number keeps the substitution pattern literally
138
+ return i < match.length ? (match[i] ?? "") : s;
139
+ }
140
+ // Named groups are `${name}` in .NET (in regex: groups are `(?<name>...)` in both)
141
+ return match.groups != null && (name as string) in match.groups ? (match.groups[name as string] ?? "") : s;
142
+ });
141
143
  }
144
+ limit = limit == null ? -1 : limit;
145
+ return input.substring(0, offset) + input.substring(offset).replace(reg as RegExp, replacer);
142
146
  }
143
147
 
144
148
  export function split(reg: string | RegExp, input: string, limit?: number, offset: number = 0) {
@@ -148,6 +152,26 @@ export function split(reg: string | RegExp, input: string, limit?: number, offse
148
152
  input = tmp;
149
153
  limit = undefined;
150
154
  }
151
- input = input.substring(offset);
152
- return input.split(reg as RegExp, limit);
155
+ // JS String.split(regex, limit) truncates the result and discards the remainder,
156
+ // whereas .NET keeps it in the last element (`limit` is the max number of elements),
157
+ // keeps the text before `offset` in the first element and includes the values of
158
+ // matched capture groups, so iterate the matches manually like .NET Regex.Split does
159
+ const result: string[] = [];
160
+ let prev = 0;
161
+ let count = limit == null ? -1 : limit - 1;
162
+ for (const m of matches(reg as RegExp, input, offset)) {
163
+ if (count === 0) {
164
+ break;
165
+ }
166
+ result.push(input.substring(prev, m.index));
167
+ prev = m.index + m[0].length;
168
+ for (let i = 1; i < m.length; i++) {
169
+ if (m[i] !== undefined) {
170
+ result.push(m[i]);
171
+ }
172
+ }
173
+ count--;
174
+ }
175
+ result.push(input.substring(prev));
176
+ return result;
153
177
  }
package/Seq.ts CHANGED
@@ -508,6 +508,94 @@ export function enumerateWhile<T>(guard: (() => boolean), xs: Iterable<T>): Iter
508
508
  return concat<Iterable<T>, T>(unfold<int32, Iterable<T>>((i: int32): Option<[Iterable<T>, int32]> => (guard() ? ([xs, i + 1] as [Iterable<T>, int32]) : undefined), 0));
509
509
  }
510
510
 
511
+ export function enumerateTryWith<T>(source: Iterable<T>, catchFilter: ((arg0: Exception) => int32), catchHandler: ((arg0: Exception) => Iterable<T>)): Iterable<T> {
512
+ return mkSeq<T>((): IEnumerator<T> => {
513
+ let e: Option<IEnumerator<T>> = undefined;
514
+ let caught = false;
515
+ let started = false;
516
+ let finished = false;
517
+ let curr: Option<T> = undefined;
518
+ const tryNext = (): boolean => {
519
+ try {
520
+ let en_2: IEnumerator<T>;
521
+ if (e == null) {
522
+ const en_1: IEnumerator<T> = getEnumerator(source);
523
+ e = en_1;
524
+ en_2 = en_1;
525
+ }
526
+ else {
527
+ en_2 = value_1(e);
528
+ }
529
+ if (en_2["System.Collections.IEnumerator.MoveNext"]()) {
530
+ curr = some(en_2["System.Collections.Generic.IEnumerator`1.get_Current"]());
531
+ return true;
532
+ }
533
+ else {
534
+ finished = true;
535
+ return false;
536
+ }
537
+ }
538
+ catch (matchValue: any) {
539
+ if (!caught) {
540
+ const ex_1: Exception = matchValue;
541
+ if (catchFilter(ex_1) !== 0) {
542
+ caught = true;
543
+ if (e == null) {
544
+ }
545
+ else {
546
+ const en_3: IEnumerator<T> = value_1(e);
547
+ try {
548
+ disposeSafe(en_3);
549
+ }
550
+ catch (matchValue_1: any) {
551
+ }
552
+ e = undefined;
553
+ }
554
+ e = getEnumerator(catchHandler(ex_1));
555
+ return tryNext();
556
+ }
557
+ else {
558
+ throw matchValue;
559
+ }
560
+ }
561
+ else {
562
+ throw matchValue;
563
+ }
564
+ }
565
+ };
566
+ return Enumerator_FromFunctions$1_$ctor_58C54629<T>((): T => {
567
+ if (!started) {
568
+ Enumerator_notStarted<void>();
569
+ }
570
+ else if (finished) {
571
+ Enumerator_alreadyFinished<void>();
572
+ }
573
+ if (curr != null) {
574
+ return value_1(curr);
575
+ }
576
+ else {
577
+ return Enumerator_alreadyFinished<T>();
578
+ }
579
+ }, (): boolean => {
580
+ if (!started) {
581
+ started = true;
582
+ }
583
+ if (finished) {
584
+ return false;
585
+ }
586
+ else {
587
+ return tryNext();
588
+ }
589
+ }, (): void => {
590
+ if (e == null) {
591
+ }
592
+ else {
593
+ disposeSafe(value_1(e));
594
+ }
595
+ });
596
+ });
597
+ }
598
+
511
599
  export function filter<T>(f: ((arg0: T) => boolean), xs: Iterable<T>): Iterable<T> {
512
600
  return choose<T, T>((x: T): Option<T> => {
513
601
  if (f(x)) {
package/Set.ts CHANGED
@@ -3,10 +3,9 @@ import { record_type, bool_type, list_type, option_type, class_type, TypeInfo }
3
3
  import { some, value as value_2, Option } from "./Option.ts";
4
4
  import { int32 } from "./Int32.ts";
5
5
  import { structuralHash, ISet, toIterator, IDisposable, disposeSafe, getEnumerator, isArrayLike, MutableArray, IEnumerator, IComparer, Exception } from "./Util.ts";
6
- import { Record } from "./Types.ts";
6
+ import { toString, Record } from "./Types.ts";
7
7
  import { fold as fold_2, cons, singleton as singleton_1, empty as empty_1, ofArrayWithTail, tail, head, isEmpty as isEmpty_1, FSharpList } from "./List.ts";
8
8
  import { fold as fold_1, fill, setItem } from "./Array.ts";
9
- import { join } from "./String.ts";
10
9
  import { NotSupportedException_$ctor_Z721C83C5 } from "./System.ts";
11
10
  import { skip, truncate, iterateIndexed, exists as exists_1, forAll as forAll_1, fold as fold_3, reduce, iterate as iterate_1, map as map_1 } from "./Seq.ts";
12
11
  import { HashSet__get_Comparer, HashSet_$ctor_Z6150332D, HashSet } from "./MutableSet.ts";
@@ -1495,7 +1494,21 @@ export class FSharpSet<T> implements ISet<T>, Iterable<T>, Iterable<T> {
1495
1494
  }
1496
1495
  toString(): string {
1497
1496
  const this$: FSharpSet<T> = this;
1498
- return ("set [" + join("; ", this$)) + "]";
1497
+ let result = "set [";
1498
+ let first = true;
1499
+ const enumerator: IEnumerator<T> = getEnumerator(this$);
1500
+ try {
1501
+ while (enumerator["System.Collections.IEnumerator.MoveNext"]()) {
1502
+ let x: T = (undefined as any), matchValue: any = (undefined as any), s: string = (undefined as any);
1503
+ const x_1: T = enumerator["System.Collections.Generic.IEnumerator`1.get_Current"]();
1504
+ result = ((first ? result : (result + "; ")) + ((x = x_1, (matchValue = x, (typeof matchValue === "string") ? ((s = (matchValue as string), ("\"" + s) + "\"")) : toString(x)))));
1505
+ first = false;
1506
+ }
1507
+ }
1508
+ finally {
1509
+ disposeSafe(enumerator as IDisposable);
1510
+ }
1511
+ return result + "]";
1499
1512
  }
1500
1513
  get [Symbol.toStringTag](): string {
1501
1514
  return "FSharpSet";
package/String.ts CHANGED
@@ -81,7 +81,7 @@ export function endsWith(str: string, pattern: string, ic: boolean | StringCompa
81
81
  return str.endsWith(pattern);
82
82
  }
83
83
  if (str.length >= pattern.length) {
84
- return cmp(str.slice(-pattern.length), pattern, ic) === 0;
84
+ return cmp(str.slice(str.length - pattern.length), pattern, ic) === 0;
85
85
  }
86
86
  return false;
87
87
  }
@@ -253,6 +253,8 @@ function formatReplacement(rep: any, flags: any, padLength: any, precision: any,
253
253
  }
254
254
  } else if (rep instanceof Date) {
255
255
  rep = dateToString(rep);
256
+ } else if (format === "A" && typeof rep === "string") {
257
+ rep = "\"" + rep + "\"";
256
258
  } else {
257
259
  rep = toString(rep);
258
260
  }
package/Types.ts CHANGED
@@ -9,18 +9,25 @@ export function seqToString<T>(self: Iterable<T>): string {
9
9
  let str = "[";
10
10
  for (const x of self) {
11
11
  if (count === 0) {
12
- str += toString(x);
12
+ str += toStringQuoted(x);
13
13
  } else if (count === 100) {
14
14
  str += "; ...";
15
15
  break;
16
16
  } else {
17
- str += "; " + toString(x);
17
+ str += "; " + toStringQuoted(x);
18
18
  }
19
19
  count++;
20
20
  }
21
21
  return str + "]";
22
22
  }
23
23
 
24
+ // Structured (%A-style) rendering of a value used as an element/field of a
25
+ // container. F#'s structured formatting (which records, unions, lists, etc.
26
+ // use in their ToString) quotes strings, e.g. `["a"; "b"]` and `{ Name = "John" }`.
27
+ function toStringQuoted(x: any, callStack = 0): string {
28
+ return typeof x === "string" ? "\"" + x + "\"" : toString(x, callStack);
29
+ }
30
+
24
31
  export function toString(x: any, callStack = 0): string {
25
32
  if (x != null && typeof x === "object") {
26
33
  if (typeof x.toString === "function" && x.toString !== Object.prototype.toString) {
@@ -31,7 +38,7 @@ export function toString(x: any, callStack = 0): string {
31
38
  const cons = Object.getPrototypeOf(x)?.constructor;
32
39
  return cons === Object && callStack < 10
33
40
  // Same format as recordToString
34
- ? "{ " + Object.entries(x).map(([k, v]) => k + " = " + toString(v, callStack + 1)).join("\n ") + " }"
41
+ ? "{ " + Object.entries(x).map(([k, v]) => k + " = " + toStringQuoted(v, callStack + 1)).join("\n ") + " }"
35
42
  : cons?.name ?? "";
36
43
  }
37
44
  }
@@ -39,23 +46,16 @@ export function toString(x: any, callStack = 0): string {
39
46
  }
40
47
 
41
48
  export function unionToString(name: string, fields: any[]) {
42
- function unionFieldToString(x: any): string {
43
- if (typeof x === "string") {
44
- return '"' + x + '"';
45
- }
46
- return toString(x);
47
- }
48
-
49
49
  if (fields.length === 0) {
50
50
  return name;
51
51
  } else {
52
52
  let fieldStr;
53
53
  let withParens = true;
54
54
  if (fields.length === 1) {
55
- fieldStr = unionFieldToString(fields[0]);
55
+ fieldStr = toStringQuoted(fields[0]);
56
56
  withParens = fieldStr.indexOf(" ") >= 0;
57
57
  } else {
58
- fieldStr = fields.map((x: any) => unionFieldToString(x)).join(", ");
58
+ fieldStr = fields.map((x: any) => toStringQuoted(x)).join(", ");
59
59
  }
60
60
  return name + (withParens ? " (" : " ") + fieldStr + (withParens ? ")" : "");
61
61
  }
@@ -119,7 +119,7 @@ function recordToJSON<T>(self: T) {
119
119
  }
120
120
 
121
121
  function recordToString<T>(self: T) {
122
- return "{ " + Object.entries(self as any).map(([k, v]) => k + " = " + toString(v)).join("\n ") + " }";
122
+ return "{ " + Object.entries(self as any).map(([k, v]) => k + " = " + toStringQuoted(v)).join("\n ") + " }";
123
123
  }
124
124
 
125
125
  function recordGetHashCode<T>(self: T) {
package/Util.ts CHANGED
@@ -564,7 +564,12 @@ export function compareDates(x: Date | IDateTime | IDateTimeOffset, y: Date | ID
564
564
  }
565
565
 
566
566
  export function comparePrimitives<T>(x: T, y: T): number {
567
- return x === y ? 0 : (x < y ? -1 : 1);
567
+ if (x === y) { return 0; }
568
+ if (x < y) { return -1; }
569
+ if (x > y) { return 1; }
570
+ // Neither equal nor ordered: at least one operand is NaN.
571
+ // Match .NET Double.CompareTo: NaN equals NaN and is less than any other value.
572
+ return Number.isNaN(x as unknown) ? (Number.isNaN(y as unknown) ? 0 : -1) : 1;
568
573
  }
569
574
 
570
575
  export function compareArraysWith<T>(x: ArrayLike<T>, y: ArrayLike<T>, comp: (x: T, y: T) => number): number {
@@ -616,7 +621,7 @@ export function compare<T>(x: T, y: T): number {
616
621
  } else if (isArrayLike(x)) {
617
622
  return isArrayLike(y) ? compareArrays(x, y) : -1;
618
623
  } else if (typeof x !== "object") {
619
- return x < y ? -1 : 1;
624
+ return comparePrimitives(x, y);
620
625
  } else if (x instanceof Date) {
621
626
  return y instanceof Date ? compareDates(x, y) : -1;
622
627
  } else {
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "private": false,
4
4
  "type": "module",
5
5
  "name": "@fable-org/fable-library-ts",
6
- "version": "2.4.0",
6
+ "version": "2.4.1",
7
7
  "description": "Core library used by F# projects compiled with fable.io",
8
8
  "author": "Fable Contributors",
9
9
  "license": "MIT",