愚かなJSジョークにうんざりしていませんか?ライブラリを作成する

JavaScriptには、「何???」という質問を引き起こすものがたくさんあります。それらのほとんどが論理的な説明を持っているという事実にもかかわらず、深く掘り下げれば、それでも驚くべきことです。しかし、JavaScriptは間違いなくとんでもない冗談に値するものではありません。たとえば、次のようなジョークが表示されることがあります。





この場合、批判は絶対に値しません。その理由を理解しましょう。






JavaScriptは、他の一般的なプログラミング言語と同様に 、単一の標準を使用して数値を表します。正確には、これは 64ビットバイナリ形式の数値に関するIEEE754標準です。他の言語で同じジョークをテストしてみましょう:



Rubyはどうですか? 0.1 + 0.2が0.3に等しくない言語は何ですか?



$ irb
irb(main):001:0> 0.1 + 0.2 == 0.3
=> false
irb(main):002:0> 0.1 + 0.2
=> 0.30000000000000004
      
      





ルビー!なんてばかげた言葉だ。



またはClojure? 0.1 + 0.2が0.3に等しくない言語は何ですか?



$ clj
Clojure 1.10.1
user=> (== (+ 0.1 0.2) 0.3)
false
user=> (+ 0.1 0.2)
0.30000000000000004

      
      





Clojure!なんてばかげた言葉だ。



または、強力なハスケルはどうですか? 0.1 + 0.2が0.3に等しくない言語は何ですか?



$ ghci
GHCi, version 8.10.1: https://www.haskell.org/ghc/  :? for help
Prelude> 0.1 + 0.2 == 0.3
False
Prelude> 0.1 + 0.2
0.30000000000000004

      
      





ハスケル!ハハハ。なんてばかげた言葉…



あなたはその考えを理解します。ここでの問題はJavaScriptではありません。これは、浮動小数点数のバイナリ表現における大きな問題です。ただし、IEEE 754の詳細については、まだ詳しく説明したくありません。任意の正確な数値が必要な場合は、JavaScriptでそれらを使用できるためです。2019年10月以降、BigIntは正式にTC39ECMAScript標準の一部になっ ています。



なぜこれを気にするのですか?



私たちは何年もの間IEEE754を使い続けました。これはほとんどの場合問題ではないようです。本当。ほとんどの場合、これは問題ではありません。しかし、それでも問題が発生する場合があります。そして、このような時には、選択肢があるのは良いことです。



たとえば、今年の初めに私はチャートのライブラリに取り組んでいました。 SVGでローソク足チャートを描きたかった。また、SVGには変換と呼ばれる優れた機能があり ます。これを要素のグループに適用すると、それらの要素の座標系が変更されます。したがって、少し注意するだけで、チャート領域の生成を簡略化できます。各ローソク足のチャートの座標を計算する代わりに、1つの変換を指定します。次に、生のデータ値を使用して各キャンドルを定義します。とてもきれい。少なくとも理論的には。



しかし、プロパティテストでは問題がありました。グラフが小さく、データ値が大きい場合、四捨五入エラーが発生します。そして、これはしばしば正常です。しかし、グラフでは、いくつかのピクセルを揃える必要があります。そうしないと、描画が間違って見えます。それで私はBigIntを学び始めました。その結果、Ratioという名前のライブラリが作成されました。そして、それがどのように書かれているかをお見せします。



フラクションクラス



浮動小数点数の問題は、それらのバイナリ表現です。コンピューターはすべての計算をバイナリ形式で実行します。また、整数の場合、このバイナリで問題ありません。問題は、10進数を表現したいときに発生します。たとえば、オーストラリアのような英語圏の国では、次のように小数を記述します。







3.1415926







ドットの左側の部分(...)は全体の部分であり、ドットの右側の部分は部分的な部分です。しかし、問題は、いくつかの数値には、簡単に2つに分割できない部分部分があることです。したがって、それらをバイナリで表すのは困難です。しかし、同じ問題がベース10でも発生します。たとえば、フラクション10/9です。次のようなものを書いてみてください。







1.111111111111111111111111111111111111.11111111111111111111111111111111111







ただし、これは概算です。10/9を正確に表すには、単位が無限でなければなりません。したがって、繰り返しを表すために他の表記法を使用する必要があります。例:これ:







1.1˙









ユニットの上のこのドットは、ユニットが継続していることを示します。しかし、ほとんどのプログラミング言語にはこの点がありません。



10/9は完全な精度を持っていることに注意してください。そして、正確であるために必要なのは、2つの情報だけです。これは 分子分母です。単一のBigInt値を使用して、任意の大きな整数を表すことができます。しかし、整数のペアを作成する 、任意の大きいまたは小さい数を表すことができ ます。



JavaScriptでは、次のようになります。



// file: ratio.js
export default class Ratio {
  // We expect n and d to be BigInt values.
  constructor(n, d) {
    this.numerator = n;
    this.denominator = d;
  }
}
      
      





だから私たちは最も難しい部分をやりました。ほぼ無限の精度で数値を表現する方法を「発明」しました。(デバイスのメモリによって制限されています。)残っているのは、数学を適用することだけです。それでは、機能を追加しましょう。



平等



最初にやりたいことは、2つの部分を比較することです。何のために?私は最初にテストを書くのが好きだからです 2つの部分が等しいかどうかを比較できれば、テストの作成ははるかに簡単です。



単純なケースでは、等式メソッドの作成は非常に簡単です。



// file: ratio.js
export default class Ratio {
  constructor(n, d) {
    this.numerator = n;
    this.denominator = d;
  }

  equals(other) {
    return (
      this.numerator === other.numerator &&
      this.denominator === other.denominator
    );
  }
}
      
      





それは良い。しかし、私たちのライブラリが1/2が2/4であると言うことができれば素晴らしいでしょう。これを行うには、分数を単純化する必要があります。つまり、等しいかどうかを確認する前に、両方の分数の分子と分母をできるだけ小さい数に減らしたいと思います。では、これをどのように行うのでしょうか?



素朴なアプローチは、1からmin(n、d)までのすべての数値を実行することです(ここで、nnとddはそれぞれ分子と分母です)。そして、これは私が最初に試したものです。コードは次のようになりました。



function simplify(numerator, denominator) {
    const maxfac = Math.min(numerator, denominator);
    for (let i=2; i<=maxfac; i++) {
      if ((numerator % i === 0) && (denominator % i === 0)) {
        return simplify(numerator / i, denominator / i);
      }
    }
    return Ratio(numerator, denominator);
}
      
      





そして、あなたが期待するように、それは信じられないほど遅いです。私のテストは永遠にかかりました。したがって、より効率的なアプローチが必要です。幸いなことに、ギリシャの数学者は数千年前にそれを発見しました。解決策は、Euclidのアルゴリズムを適用することです。これは、2つの整数の最大の共通除数を見つける方法です。



Euclidのアルゴリズムの再帰バージョンは美しくエレガントです。



function gcd(a, b) {
    return (b === 0) ? a : gcd(b, a % b);
}
      
      





メモ化が適用可能であるため、アルゴリズムは非常に魅力的です。しかし、 残念ながら、V8またはSpiderMonkeyにはまだテール再帰がありません(少なくともこの記事の執筆時点ではありません。)これは、十分な大きさの整数で実行すると、スタックオーバーフローが発生することを意味します。大きな整数は出発点のようなものです。



したがって、代わりに反復バージョンを使用しましょう。



// file: ratio.js
function gcd(a, b) {
    let t;
    while (b !== 0) {
        t = b;
        b = a % b;
        a = t;
    }
    return a;
}
      
      





それほどエレガントではありませんが、仕事をします。そして、このコードを使用して、分数を単純化する関数を記述できます。これを行っている間、分母が常に正になるように小さな変更を加えます(つまり、負の数の場合、分子のみが符号を変更します)。



// file: ratio.js

function sign(x) {
  return x === BigInt(0) ? BigInt(0)
       : x > BigInt(0)   ? BigInt(1) 
       /* otherwise */   : BigInt(-1);
}

function abs(x) {
  return x < BigInt(0) ? x * BigInt(-1) : x;
}

function simplify(numerator, denominator) {
  const sgn = sign(numerator) * sign(denominator);
  const n = abs(numerator);
  const d = abs(denominator);
  const f = gcd(n, d);
  return new Ratio((sgn * n) / f, d / f);
}

      
      





そして今、私たちは私たちの平等法を書くことができます:



// file: ratio.js -- inside the class declaration
  equals(other) {
    const a = simplify(this);
    const b = simplify(other);
    return (
      a.numerator === b.numerator &&
      a.denominator === b.denominator
    );
  }

      
      





これで、2つの分数が等しいかどうかを比較できます。それほど多くないように聞こえるかもしれませんが、ユニットテストを記述して、ライブラリが期待どおりに機能することを確認できることを意味します。



他のタイプへの変換



私のライブラリにあるすべてのユニットテストを書き出すことであなたを退屈させることはありません。しかし、分数を他の形式に変換するとよいでしょう。たとえば、デバッグメッセージでそれらを文字列として表現したい場合があります。あるいは、それらを数値に変換したいのかもしれません。それでは、クラスの.toString()メソッドと.toValue()メソッドをオーバーライドしましょう。



.toString()メソッドが最も簡単なので、それから始めましょう。



// file: ratio.js -- inside the class declaration
  toString() {
    return `${this.numerator}/${this.denominator}`;
  }
      
      





十分に単純です。しかし、数値に戻すのはどうですか?これを行う1つの方法は、分子を分母で単純に除算することです。



// file: ratio.js -- inside the class declaration
  toValue() {
    return  Number(this.numerator) / Number(this.denominator);
  }
      
      





これはよく機能します。ただし、コードを少し調整したい場合があります。私たちのライブラリの要点は、必要な精度を得るために大きな整数を使用することです。また、これらの整数が大きすぎて数値に戻すことができない場合があります。ただし、可能な場合は、Numberをできるだけ真実に近づけたいと考えています。したがって、BigIntをNumberに変換する際に、いくつかの計算を行います。



// file: ratio.js -- inside the class declaration
  toValue() {
    const intPart = this.numerator / this.denominator;
    return (
      Number(this.numerator - intPart * this.denominator) /
        Number(this.denominator) + Number(intPart)
    );
  }
      
      





整数部分を抽出することにより、BigInt値をNumberに変換する前にサイズを縮小します。これを行うには、範囲の問題が少ない他の方法があります。それらは一般的に難しく、遅くなります。興味のある方は、詳しく調べてみることをお勧めします。しかし、この記事では、簡単なアプローチで十分なケースをカバーしています。



乗算と除算



数字で何かしましょう。乗算と除算はどうですか?フラクションは簡単です。分子に分子を掛け、分母に分母を掛けます。



// file: ratio.js -- inside the class declaration
  times(x) {
    return simplify(
      x.numerator * this.numerator,
      x.denominator * this.denominator
    );
  }
      
      





分割は上記のコードに似ています。2番目の部分を反転してから、乗算します。



// file: ratio.js -- inside the class declaration
  divideBy(x) {
    return simplify(
      this.numerator * x.denominator,
      this.denominator * x.numerator
    );
  }
      
      





足し算と引き算



これで、乗算と除算ができました。論理的には、次に書くのは加算と減算です。これは、乗算と除算よりも少し複雑です。しかしあまりありません。

2つの分数を追加するには、最初にそれらを同じ分母に持ってきてから、分子を追加する必要があります。コードでは、次のようになります。



// file: ratio.js -- inside the class declaration
  add(x) {
    return simplify(
      this.numerator * x.denominator + x.numerator * this.denominator,
      this.denominator * x.denominator
    );
  }
      
      





すべてに分母が掛けられます。また、simplify()を使用して、分子と分母の数の観点から分数をできるだけ小さくします。

減算は加算に似ています。以前と同じ分母が揃うように、2つの分数を操作しています。次に、加算ではなく減算します。



// file: ratio.js -- inside the class declaration
  subtract(x) {
    return simplify(
      this.numerator * x.denominator - x.numerator * this.denominator,
      this.denominator * x.denominator
    );
  }
      
      





つまり、基本的な演算子があります。加算、減算、乗算、除算ができます。しかし、他にもいくつかの方法が必要です。特に、数字には重要な特性があります。それらを互いに比較することができます。



比較



.equals()についてはすでに説明しました。しかし、私たちは単なる平等以上のものを必要としています。また、大きい方と小さい方の比率を定義できるようにしたいと思います。したがって、あるフラクションが別のフラクション以下であるかどうかを示す.lte()メソッドを作成します。 .equals()と同様に、2つのうちどちらが小さいかは明らかではありません。それらを比較するには、両方を同じ分母に変換してから、分子を比較する必要があります。少し単純化しすぎると、次のようになります。



// file: ratio.js -- inside the class declaration
  lte(other) {
    const { numerator: thisN, denominator: thisD } = simplify(
      this.numerator,
      this.denominator
    );
    const { numerator: otherN, denominator: otherD } = simplify(
      other.numerator,
      other.denominator
    );
    return thisN * otherD <= otherN * thisD;
  }

      
      





.lte()と.equals()を取得したら、残りの比較を出力できます。任意の比較演算子を選択できます。しかし、等しい()と>、<、≥または≤がある場合、ブール論理を使用して残りを推測できます。この場合、FantasyLand標準がlte()を使用するため、lte()を選択 しました他の演算子は次のようになります。



// file: ratio.js -- inside the class declaration
  lt(other) {
    return this.lte(other) && !this.equals(other);
  }

  gt(other) {
    return !this.lte(other);
  }

  gte(other) {
    return this.gt(other) || this.equals(other);
  }

      
      





丸め



これで、分数を比較できます。また、乗算と除算、加算と減算も可能です。しかし、ライブラリをもっと楽しくするためには、もっと多くのツールが必要です。JavaScript Mathコンビニエンスオブジェクトには、.floor()メソッドと.ceil()メソッドが含まれています。

.floor()から始めましょう。フロアは値を取り、それを切り捨てます。正の数の場合、これは、部分全体を保持し、残りを破棄することを意味します。ただし、負の数の場合はゼロから切り上げるため、負の数にはもう少し注意を払う必要があります。



// file: ratio.js -- inside the class declaration
  floor() {
    const one = new Ratio(BigInt(1), BigInt(0));
    const trunc = simplify(this.numerator / this.denominator, BigInt(1));
    if (this.gte(one) || trunc.equals(this)) {
      return trunc;
    }
    return trunc.minus(one);
  }
      
      





これで、上記のコードを使用して、切り上げられた値を計算できます。



// file: ratio.js -- inside the class declaration
  ceil() {
    const one = new Ratio(BigInt(1), BigInt(0));
    return this.equals(this.floor()) ? this : this.floor().add(one);
  }
      
      





これで、多くの数学的操作に必要なもののほとんどが手に入りました。また、.toValue()を使用すると、計算を簡単に10進数に戻すことができます。しかし、浮動小数点数を分数に変換したい場合はどうでしょうか。



分数への数



数値を分数に変換することは、一見したところよりも困難です。そして、この変換を行うには多くの異なる方法があります。私の実装は最も正確ではありませんが、それで十分です。それを機能させるために、最初に数値を文字列に変換します。これは、ご存じのとおり、シーケンスの形式を取ります。これを行うために、JavaScriptは.toExponential()メソッドを提供します。このメソッドは、指数表記の数値を返します。アイデアを理解するのに役立ついくつかの例を次に示します。



let x = 12.345;
console.log(x.toExponential(5));
// ⦘ '1.23450e+1''

x = 0.000000000042;
console.log(x.toExponential(3));
// ⦘ '4.200e-11'

x = 123456789;
console.log(x.toExponential(4));
// ⦘ '1.2346e+8'

      
      





このコードは、数値を正規化された10進値と乗数として表すことによって機能します。正規化された10進ビットはマンティッサと呼ばれ、係数は指数と呼ばれます。ここで「正規化」とは、マンティッサの絶対値が常に10未満であることを意味し、指数は常に10になります。因子の開始を文字「e」(「指数」の略)で示します。



この表記の利点は、一貫性があることです。小数点の左側には常に正確に1桁の数字があります。また、.toExponential()を使用すると、必要な有効桁数を指定できます。次に「e」が来て、指数は常に整数です。値はシーケンシャルなので、生意気なregexを使用して解析できます。



プロセスは次のようになります。前述のように、.toExponential()は、有効桁数を指定するパラメーターを取ります。できるだけ多くの数字が必要です。そのため、精度を100に設定します(これはほとんどのJavaScriptエンジンで許可されています)。ただし、この例では、精度10を使用します。ここで、番号0.987654321e0があるとします。小数点を10桁右に移動します。これにより、9876543210が得られます。次に、10 ^ 10で割ると、9876543210/100000000が得られます。これにより、987654321/100000000に簡略化されます。



しかし、私たちはこの出展者に注意を払う必要があります。0.987654321e9のような数値がある場合でも、小数点を10桁右にシフトします。しかし、10で割って10-9 = 1の累乗にします。







0.987654321×109=9876543210/101=











987654321/1







そのように保つために、いくつかのヘルパー関数を定義しました。



// Transform a ‘+’ or ‘-‘ character to +1 or -1
function pm(c) {
  return parseFloat(c + "1");
}

// Create a new bigint of 10^n. This turns out to be a bit
// faster than multiplying.
function exp10(n) {
  return BigInt(`1${[...new Array(n)].map(() => 0).join("")}`);
}
      
      





彼らの助けを借りて、fromNumber()関数全体をつなぎ合わせることができます。



// file: ratio.js -- inside the class declaration
  static fromNumber(x) {
    const expParse = /(-?\d)\.(\d+)e([-+])(\d+)/;
    const [, n, decimals, sgn, pow] =
      x.toExponential(PRECISION).match(expParse) || [];
    const exp = PRECISION - pm(sgn) * +pow;
    return exp < 0
      ? simplify(BigInt(`${n}${decimals}`) * exp10(-1 * exp), BigInt(1))
      : simplify(BigInt(`${n}${decimals}`), exp10(exp));
  }

      
      





基本的な機能のほとんどがカバーされています。数字から分数へ、そしてまた戻ることができます。しかし、私の特定のアプリケーションでは、もっと必要でした。特に、指数と対数を見つける必要がありました。



指数化



指数化とは、数値がそれ自体で何度も乗算されることです。たとえば、2 ^ 3 = 2×2×2 = 8です。次数が整数である単純なケースには、組み込みのBigInt:**演算子があります。したがって、分数を累乗する場合、それは良いオプションです。これは、分数が累乗される方法です。





(xy)n=xnyn







したがって、指数化メソッドの最初のチャンクは次のようになります。



// file: ratio.js -- inside the class declaration
  pow(exponent) {
    if (exponent.denominator === BigInt(1)) {
        return simplify(
            this.numerator ** exponent.numerator,
            this.denominator ** exponent.numerator
        );
    }
  }
      
      





よく働く。まあ...ほとんど良い。今、物事はより複雑になります。担保と数学の制約の外で、私たちはいくつかのトレードオフをしなければなりません。妥当な時間枠内で応答を得るには、精度を犠牲にする必要がある場合があります。



指数化は簡単に大きな数を生成します。そして、数字が大きくなると、物事は遅くなります。この記事を書いている間、私はまた、何日もかけて完了しなかった計算を書きました。したがって、注意する必要があります。しかし、それは大丈夫です。すべてがBigIntにもたらされます。



しかし、別の問題があります。学位の分母が1つでない場合はどうなりますか?たとえば、8 ^(2/3)を計算したい場合はどうなりますか?



幸い、この問題を2つの小さな問題に分割できます。ある部分を別の部分の累乗に減らしたいのです。たとえば、x / yをa / bに関連付けることができます。指数の法則は、以下が同等であると述べています。





(xy)ab=((xy)1b)a=(x1by1b)a







あるBigIntを別のBigIntのパワーに変換する方法はすでに知っています。しかし、分数度はどうですか?まあ、別の同等物があります:





x1n=xn







つまり、xxを1n1nの累乗に減らすことは、xxのn番目のルートを見つけることと同じです。これは、BigIntのn番目のルートを計算する方法が見つかった場合、任意の次数を計算できることを意味します。



よく考えられたWeb検索を使用すると、n番目のルートを推定するためのアルゴリズムを見つけるのにそれほど時間はかかりません。最も一般的な方法は ニュートンの方法です。それは評価、rrから機能します。次に、最適な見積もりを取得するための計算が行われます。





rx1nr=1n((n1)r+(xrn1))







必要な精度に達するまで、これらの計算を繰り返します。残念ながら、有限の割合として表すことができないいくつかの根があります。言い換えれば、完全な精度を得るには、無限に長いBigInt値が必要です。実際には、これは任意の反復制約を選択する必要があることを意味します。



ここに戻ります。とりあえず、適度に正確なn番目のルートを計算する方法を考えてみましょう。推定値rrは分数になるため、次のように記述できます。





r=ab.







そして、これにより、次のように計算を書き直すことができます。





ab=(n1)an+xbnnban1







これで、すべてが整数計算の観点から行われ、BigIntでの使用に適しています。上記のr'r 'の式にababを自由に挿入して、私の結果を確認してください。 JavaScriptでは、次のようになります。



const estimate = [...new Array(NUM_ITERATIONS)].reduce(r => {
  return simplify(
    (n - BigInt(1)) * r.numerator ** n + x * r.denominator ** n,
    n * r.denominator * r.numerator ** (n - BigInt(1))
  );
}, INITIAL_ESTIMATE);
      
      





n番目のルートの推定に適した精度に達するまで、この計算を繰り返すだけです。問題は、定数に適した値を考え出す必要があることです。つまり、NUM_ITERATIONSとINITIAL_ESTIMATEです。

多くのアルゴリズムは、INITIAL_ESTIMATEのものから始まります。これは賢い選択です。多くの場合、n番目のルートが何であるかを推測する良い方法がありません。しかし、「スナッグ」と書きましょう。 (今のところ)分子と分母がNumberの範囲にあると仮定しましょう。次に、Math.pow()を使用して初期スコアを取得できます。次のようになります。



// Get an initial estimate using floating point math
// Recall that x is a bigint value and n is the desired root.
const initialEstimate = Ratio.fromNumber(
    Math.pow(Number(x), 1 / Number(n))
);
      
      





したがって、最初の評価には価値があります。 NUM_ITERATIONはどうですか?さて、実際には、私がしたことは、10の仮定から始めました。そして、私は特性テストをしました。計算が妥当な時間枠内になるまで、数を増やし続けました。そして、ついに機能した図... 1.1回の繰り返し。これは私を少し悲しませますが、浮動小数点計算よりも少し正確です。実際には、多くの分数の累乗を計算していない場合は、この数を増やすことができます。



簡単にするために、n番目のルートの計算を別の関数に抽出します。すべてをまとめると、コードは次のようになります。



// file: ratio.js -- inside the class declaration
  static nthRoot(x, n) {
    // Handle special cases
    if (x === BigInt(1)) return new Ratio(BigInt(1), BigInt(1));
    if (x === BigInt(0)) return new Ratio(BigInt(0), BigInt(1));
    if (x < 0) return new Ratio(BigInt(1), BigInt(0)); // Infinity

    // Get an initial estimate using floating point math
    const initialEstimate = Ratio.fromNumber(
      Math.pow(Number(x), 1 / Number(n))
    );

    const NUM_ITERATIONS = 1;
    return [...new Array(NUM_ITERATIONS)].reduce((r) => {
      return simplify(
        n -
          BigInt(1) * (r.numerator ** n) +
          x * (r.denominator ** n),
        n * r.denominator * r.numerator ** (n - BigInt(1))
      );
    }, initialEstimate);
  }

  pow(n) {
    const { numerator: nNumerator, denominator: nDenominator } = n.simplify();
    const { numerator, denominator } = this.simplify();
    if (nNumerator < 0) return this.invert().pow(n.abs());
    if (nNumerator === BigInt(0)) return Ratio.one;
    if (nDenominator === BigInt(1)) {
      return new Ratio(numerator ** nNumerator, denominator ** nNumerator);
    }
    if (numerator < 0 && nDenominator !== BigInt(1)) {
      return Ratio.infinity;
    }

    const { numerator: newN, denominator: newD } = Ratio.nthRoot(
      numerator,
      nDenominator
    ).divideBy(Ratio.nthRoot(denominator, nDenominator));
    return new Ratio(newN ** nNumerator, newD ** nNumerator);
  }

      
      





不完全で遅い。しかし、そのタスクはおおむね実行可能になっています。Number.MAX_VALUEより大きい整数がある場合、どのように推定値を取得するかという問題が残ります。ただし、これは読者の演習として残しておきます。この記事はすでに長すぎます。



対数



私は認めなければなりません、対数は数週間私を困惑させました。私の開発では、10を底とする対数を計算する必要があるので、対数を計算するためのアルゴリズムを探しました。そしてそれらの多くがあります。しかし、数学ライブラリに含めるのに十分に機能するものを見つけることができませんでした。



なんでそんなに難しいの?私の目標は、浮動小数点数よりも高い精度で対数を計算することでした。そうでなければ、なぜこれすべて?浮動小数点対数関数、Math.log10()、高速で組み込み。そこで、対数を繰り返し計算する方法を提供するアルゴリズムを調べました。そして、彼らは働きます。ただし、浮動小数点の精度よりも高くなるのは遅いです。少し遅いだけではありません。はるかに遅い。



繰り返しを行うと、作成する部分がより正確になります。しかし、この精度には代償が伴います。分数のBigInt値はますます大きくなっています。そして、大きくなるにつれて、増殖するのに長い時間がかかり始めます。ある時点で、私は計算を3日間残しました。しかし、計算が行われている間、私は何かを思い出しました。



グラフの適切なスケーリング値を計算できるようにするには、log10()メソッドが必要だったことを思い出しました。そして、これらの計算では、.log10()を呼び出すたびに、すぐに.floor()を呼び出しました。これは、対数の整数部分のみが必要であることを意味します。対数を小数点以下100桁まで計算することは、時間と労力の無駄でした。



さらに、10を底とする対数の全体を計算する簡単な方法があり、必要なのは数を数えることだけです。素朴な試みは次のようになります。



// file: ratio.js -- inside the class declaration
  floorLog10() {
    return simplify(BigInt((this.numerator / this.denominator).toString().length - 1), BigInt(1));
  }
      
      





残念ながら、これは1未満の値では機能しません。ただし、それでも、いくつかの対数法則を使用してそのような値を処理できます。





log10(ab)=log10(a)log10(b)log10(1x)=log10(1)log10(x)=log10(x)







したがって:





log10(ba)=log10(ab)







すべてをまとめると、より堅牢なfloorLog10()メソッドが得られます。



// file: ratio.js -- inside the class declaration

  invert() {
    return simplify(this.denominator, this.numerator);
  }

  floorLog10() {
    if (this.equals(simplify(BigInt(0), BigInt(1)))) {
      return new Ratio(BigInt(-1), BigInt(0));
    }
    return this.numerator >= this.denominator
      ? simplify((this.numerator / this.denominator).toString().length - 1, 1)
      : simplify(BigInt(-1), BigInt(1)).subtract(this.invert().floorLog10());
  }
      
      





再び。なぜ苦しむのですか?



現時点では、ライブラリには、グラフを操作するために、アプリケーションに必要なすべての機能があります。しかし、それでも面白いかもしれません、なぜこのすべての問題?すでにいくつかの任意の精度ライブラリがあります。それらの1つを使用して、それで済ませてみませんか?



正直なところ、ほとんどの場合、既存のライブラリを使用します。特に急いでいる場合は。誰かがすでに優れた仕事をしているのであれば、これらすべてを行う意味はありません。



ここでのキーワードは優れています。そして、ここで自分のライブラリを書きたいという私の動機が作用します。上記のfloorLog10()メソッドは完璧な例です。それは私がやりたいことに必要な正確な計算を提供します。約6行のコードで効率的に実行します。



他の人のライブラリを使用する場合、次の2つのいずれかに遭遇します。



  1. 開発者は、log10()またはその他の対数法を実装しませんでした。


または



  1. 開発者は、log10()メソッド(または同等のもの)実装しました。


最初のシナリオでは、floorLog10()を作成する必要があります。 2番目の状況では、おそらく対数法を使用します。そして、私のコードは本来よりも遅く、複雑になります。



独自のライブラリを作成することで、それを自分のアプリケーションに適合させることができます。もちろん、他の人はコードが役に立つと思うかもしれませんが、私は彼らのニーズに対して責任がありません。そのため、私のアプリケーションは、決して使用しない複雑なコードを持ち歩く必要がありません。



これらすべてに加えて、私は自分のライブラリを作成することで多くのことを学びました。私は今、BigIntの制限を以前よりも実際にはるかによく理解しています。 n番目のルートメソッドのパフォーマンスを調整できることはわかっています。実行している計算の量と必要な精度に応じて調整できます。



独自の汎用ライブラリを作成する価値がある場合もあります。コードを開く予定がない場合でも。誰も使っていなくても。あなたはたくさん学ぶことができ、それはまた喜びをもたらすことができます。



浮動小数点の問題について詳しく知りたい場合は、0.30000000000000004.comを参照して くださいまた、ライブラリ全体を表示して計算を実行したい場合は、コードを使用してこのサンドボックスを確認でき ます



画像






その他の職業やコース


















All Articles