الانتقال إلى المحتوى الرئيسي
تحتوي مجموعة البيانات هذه على جميع الـ commits والتغييرات الخاصة بمستودع ClickHouse. ويمكن إنشاؤها باستخدام أداة git-import الأصلية الموزعة مع ClickHouse. توفّر البيانات الناتجة ملف tsv لكل جدول من الجداول التالية:
  • commits - عمليات commit مع الإحصاءات.
  • file_changes - الملفات التي تغيّرت في كل commit، مع معلومات عن التغيير والإحصاءات.
  • line_changes - كل سطر تغيّر في كل ملف تغيّر في كل commit، مع معلومات كاملة عن السطر ومعلومات عن آخر تغيير سابق لهذا السطر.
اعتبارًا من 8 نوفمبر 2022، كان الحجم التقريبي لكل ملف TSV وعدد الصفوف فيه كما يلي:
  • commits - 7.8M - 266,051 صفًا
  • file_changes - 53M - 266,051 صفًا
  • line_changes - 2.7G - 7,535,157 صفًا

توليد البيانات

هذا أمر اختياري. نحن نوفر البيانات مجانًا — راجع تنزيل البيانات وإدخالها.
سيستغرق هذا نحو 3 دقائق (اعتبارًا من 8 نوفمبر 2022 على جهاز MacBook Pro 2021) لإكماله لمستودع ClickHouse. يمكن الحصول على قائمة كاملة بالخيارات المتاحة من المساعدة المدمجة في الأداة.
يوفّر هذا القسم أيضًا عبارات DDL لكل جدول من الجداول المذكورة أعلاه، على سبيل المثال:
يُفترض أن تعمل هذه الاستعلامات مع أي مستودع. لا تتردد في الاستكشاف وإبلاغنا بما تتوصل إليه بعض الإرشادات المتعلقة بأوقات التنفيذ (اعتبارًا من نوفمبر 2022):
  • Linux - ~/clickhouse git-import - 160 دقيقة

تنزيل البيانات وإدراجها

يمكن استخدام البيانات التالية لإعادة إنشاء بيئة عمل فعّالة. وبدلًا من ذلك، تتوفر مجموعة البيانات هذه على play.clickhouse.com - راجع الاستعلامات لمزيد من التفاصيل. يمكن العثور أدناه على الملفات التي تم إنشاؤها للمستودعات التالية: لإدراج هذه البيانات، جهّز قاعدة البيانات بتنفيذ الاستعلامات التالية:
أدرِج البيانات باستخدام INSERT INTO SELECT ودالة s3. على سبيل المثال، نُدرِج أدناه ملفات ClickHouse في الجداول المقابلة لها: commits
file_changes
line_changes

الاستعلامات

تقترح الأداة عدة استعلامات من خلال مخرجات المساعدة الخاصة بها. وقد أجبنا عن هذه الاستعلامات، إلى جانب بعض الأسئلة الإضافية ذات الاهتمام. وهذه الاستعلامات مرتبة تقريبًا حسب تزايد مستوى التعقيد، بخلاف الترتيب العشوائي الذي تعرضه الأداة. مجموعة البيانات هذه متاحة في play.clickhouse.com ضمن قواعد البيانات git_clickhouse. ونوفر رابطًا إلى هذه البيئة لجميع الاستعلامات، مع تكييف اسم قاعدة البيانات حسب الحاجة. يُرجى ملاحظة أن نتائج play قد تختلف عن تلك المعروضة هنا بسبب اختلاف وقت جمع البيانات.

سجلّ ملف واحد

أبسط الاستعلامات. هنا نستعرض جميع رسائل commit الخاصة بالملف StorageReplicatedMergeTree.cpp. ونظرًا إلى أن الأحدث غالبًا ما تكون أكثر إثارة للاهتمام، فإننا نفرزها بحيث تظهر أحدث الرسائل أولًا. تشغيل
يمكننا أيضًا مراجعة تغييرات الأسطر، مع استبعاد عمليات إعادة التسمية؛ أي إننا لن نعرض التغييرات التي سبقت عملية إعادة التسمية عندما كان الملف موجودًا باسم مختلف: تشغيل
لاحظ أن هناك صيغة أكثر تعقيدًا لهذا الاستعلام، إذ نعثر فيها على سجل الـ commit لملف سطرًا بسطر مع أخذ عمليات إعادة التسمية في الحسبان.

العثور على الملفات الحالية النشطة

هذا مهم للتحليل اللاحق عندما نريد أن نأخذ في الاعتبار فقط الملفات الحالية في المستودع. ونقدّر هذه المجموعة بأنها الملفات التي لم تُعَد تسميتها أو تُحذف (ثم تُضاف مرة أخرى/تُعاد تسميتها مرة أخرى). ملاحظة: يبدو أن سجل commit قد تعرّض لانقطاع فيما يتعلق بالملفات ضمن الأدلة dbms وlibs وtests/testflows/ أثناء إعادة تسميتها. لذلك نستبعد هذه أيضًا. تشغيل
لاحظ أن هذا يتيح إعادة تسمية الملفات ثم إعادتها لاحقًا إلى أسمائها الأصلية. أولًا، نجمع old_path للحصول على قائمة بالملفات المحذوفة نتيجة إعادة التسمية. ثم نضمّ هذه القائمة إلى آخر عملية لكل path. وأخيرًا، نرشّح هذه القائمة للاحتفاظ فقط بالعناصر التي لا يكون فيها الحدث الأخير Delete. تشغيل
لاحظ أننا تخطّينا استيراد عدة أدلة أثناء عملية الاستيراد، أي: --skip-paths 'generated\.cpp|^(contrib|docs?|website|libs/(libcityhash|liblz4|libdivide|libvectorclass|libdouble-conversion|libcpuid|libzstd|libfarmhash|libmetrohash|libpoco|libwidechar_width))/' يُظهر تطبيق هذا النمط على git list-files العدد 18155.
لذا فإن حلّنا الحالي ليس سوى تقدير للملفات الحالية الاختلاف هنا ناتج عن عدة عوامل:
  • قد تحدث إعادة التسمية بالتزامن مع تعديلات أخرى على الملف. وتُسجَّل هذه كأحداث منفصلة في file_changes ولكن في الوقت نفسه. ولا تملك الدالة argMax أي طريقة للتمييز بينها، لذا تختار أول قيمة. كما أن الترتيب الطبيعي لعمليات الإدراج (وهو الوسيلة الوحيدة لمعرفة الترتيب الصحيح) لا يُحفَظ عبر عملية union، لذلك قد تُختار أحداث التعديل. على سبيل المثال، في الأسفل خضع الملف src/Functions/geometryFromColumn.h لعدة تعديلات قبل إعادة تسميته إلى src/Functions/geometryConverters.h. وقد يختار حلّنا الحالي حدث Modify على أنه أحدث تغيير، مما يؤدي إلى الإبقاء على src/Functions/geometryFromColumn.h.
تشغيل
  • سجلّ تاريخ commit معطّل - أحداث الحذف مفقودة. لم يُحدَّد المصدر والسبب بعد.
لا ينبغي أن تؤثر هذه الفروقات بشكل ملحوظ في تحليلنا. نرحّب بإصدارات محسّنة من هذا الاستعلام.

عرض الملفات الأكثر تعديلًا

مع الاقتصار على الملفات الحالية، نعتبر عدد التعديلات هو مجموع عمليات الحذف والإضافة. تشغيل

في أي يوم من أيام الأسبوع تُجرى عمليات commit عادةً؟

تشغيل
يبدو ذلك منطقيًا مع بعض التراجع في الإنتاجية يوم الجمعة. ومن الرائع رؤية الناس يثبتون تغييرات الكود في عطلات نهاية الأسبوع! شكرًا جزيلًا لمساهمينا!

سجل الدليل الفرعي/الملف - عدد الأسطر وعمليات commit والمساهمين بمرور الوقت

سينتج عن ذلك نتيجة استعلام كبيرة يصعب عرضها أو تصورها بشكل عملي من دون تصفية. لذلك نتيح في المثال التالي تطبيق تصفية على ملف أو دليل فرعي. هنا نُجري Group By حسب الأسبوع باستخدام الدالة toStartOfWeek — عدِّل ذلك حسب الحاجة. تشغيل
يمكن تمثيل هذه البيانات بصريًا بشكل جيد. نستخدم أدناه Superset. بالنسبة إلى الأسطر المضافة والمحذوفة: بالنسبة إلى عمليات commit والمؤلفين:

اعرض الملفات التي تضم أكبر عدد من المؤلفين

اقتصر على الملفات الحالية فقط. جرّب

أقدم أسطر الشيفرة في المستودع

يقتصر على الملفات الحالية فقط. جرّب

الملفات ذات السجلّ الأطول

يقتصر على الملفات الحالية فقط. جرّب
بنية البيانات الأساسية لدينا، Merge Tree، تشهد بطبيعة الحال تطورًا مستمرًا ولها سجل طويل من التعديلات!

توزيع المساهمين بين التوثيق والشفرة البرمجية على مدار الشهر

أثناء جمع البيانات، جرى استبعاد التغييرات في المجلد docs/ بسبب السجل شديد الفوضى للالتزامات (commit). لذلك، فإن نتائج هذا الاستعلام غير دقيقة. هل نكتب مزيدًا من التوثيق في أوقات معيّنة من الشهر، مثلًا قرب تواريخ الإصدار؟ يمكننا استخدام الدالة countIf لحساب نسبة بسيطة، ثم عرض النتيجة بصريًا باستخدام الدالة bar. جرّب
ربما يزداد قليلًا قرب نهاية الشهر، لكننا نحافظ عمومًا على توزيع متوازن جيد. ومرة أخرى، فهذا غير موثوق بسبب التصفية التي يطبّقها مرشّح الوثائق أثناء إدخال البيانات.

المؤلفون الأكثر تنوعًا في الأثر

نقصد بالتنوع هنا عدد الملفات الفريدة التي ساهم فيها المؤلف. جرّب
لنرَ مَن لديه أكثر الالتزامات تنوعًا في أعماله الأخيرة. وبدلًا من التقييد حسب التاريخ، سنكتفي بآخر N من الالتزامات الخاصة بمؤلف معيّن (في هذه الحالة استخدمنا 3، لكن لا تتردد في تعديل ذلك): جرّب

الملفات المفضلة لدى مؤلف

هنا نختار مؤسسنا Alexey Milovidov ونقصر تحليلنا على الملفات الحالية. جرّب
هذا منطقي لأن Alexey كان مسؤولًا عن صيانة سجل التغييرات. لكن ماذا لو استخدمنا الاسم الأساسي للملف لتحديد ملفاته الأكثر شيوعًا؟ يتيح ذلك أخذ عمليات إعادة التسمية في الحسبان، ويُفترض أن يركّز على المساهمات البرمجية. جرّب
لعل هذا يعكس مجالات اهتمامه بدرجة أكبر.

أكبر الملفات ذات أقل عدد من المؤلفين

لهذا، نحتاج أولًا إلى تحديد أكبر الملفات. سيكون تقدير ذلك من خلال إعادة بناء كل ملف بالكامل استنادًا إلى سجل الالتزامات مكلفًا جدًا! ولإجراء هذا التقدير، وبافتراض أننا نحصره في الملفات الحالية، نجمع الأسطر المضافة ونطرح الأسطر المحذوفة. بعد ذلك، يمكننا حساب نسبة الطول إلى عدد المؤلفين. جرّب
قد لا تكون القواميس النصية واقعية، لذا لنقتصر على الشيفرة فقط باستخدام مُرشِّح امتداد الملف! جرّب
يوجد هنا قدر من الانحياز للأحدث — إذ إن الملفات الأحدث أتيحت لها فرص أقل لعمليات التزام. ماذا لو قصرنا ذلك على الملفات التي لا يقل عمرها عن سنة واحدة؟ جرّب

توزيع الـ التزام وأسطر الشيفرة حسب الوقت؛ وحسب يوم الأسبوع، وحسب المؤلف؛ ولأدلة فرعية محددة

نفسّر ذلك على أنه عدد الأسطر المضافة والمحذوفة بحسب يوم الأسبوع. في هذه الحالة، نركّز على دليل Functions جرّب
وحسب الوقت من اليوم، جرّب
يبدو هذا التوزيع منطقيًا، نظرًا إلى أن معظم فريق التطوير لدينا في أمستردام. وتساعدنا دوال bar على تصوّر هذه التوزيعات: جرّب

مصفوفة للمؤلفين تُظهر أيّ المؤلفين يميلون إلى إعادة كتابة شيفرة مؤلفين آخرين

تشير القيمة sign = -1 إلى حذف شيفرة. نستبعد علامات الترقيم وإدراج الأسطر الفارغة. جرّب
يتيح مخطط Sankey ‏(SuperSet) عرض ذلك بصورة واضحة وجميلة. لاحظ أننا نرفع قيمة LIMIT BY إلى 3 للحصول على أبرز 3 أشخاص يزيلون الشيفرة لكل مؤلف، مما يعزّز التنوع في التصور المرئي. من الواضح أن Alexey يميل إلى إزالة شيفرة الآخرين. لنستبعده للحصول على عرض أكثر توازنًا لعمليات إزالة الشيفرة.

من هو المساهم ذو أعلى نسبة مئوية في كل يوم من أيام الأسبوع؟

إذا اكتفينا بالنظر إلى عدد عمليات commit فقط: تشغيل
حسنًا، يبدو أن هناك بعض الأفضلية هنا للمساهم الأقدم لدينا، مؤسسنا Alexey. لنقصر تحليلنا على العام الماضي. جرّب
لا يزال هذا مبسّطًا بعض الشيء ولا يعكس عمل الأشخاص. قد يكون المقياس الأفضل هو معرفة مَن صاحب أكبر مساهمة في كل يوم، كنسبة من إجمالي العمل المنجَز خلال العام الماضي. لاحظ أننا نتعامل مع حذف الشيفرة وإضافتها على قدم المساواة. جرّب

توزيع أعمار الشيفرة في المستودع

نقصر التحليل على الملفات الحالية. وللاختصار، نقيّد النتائج بعمق 2، مع 5 ملفات لكل مجلد جذري. عدّل ذلك حسب الحاجة. تشغيل

ما النسبة المئوية من الشيفرة البرمجية التي كتبها مؤلف وأزالها مؤلفون آخرون؟

بالنسبة إلى هذا السؤال، نحتاج إلى عدد الأسطر التي كتبها مؤلف مقسومًا على العدد الإجمالي للأسطر التي أزالها مساهم آخر. تشغيل

سرد الملفات التي أُعيدت كتابتها أكبر عدد من المرات؟

قد يكون أبسط نهج للإجابة عن هذا السؤال هو ببساطة حساب أكبر عدد من تعديلات الأسطر لكل مسار (مع الاقتصار على الملفات الحالية)، على سبيل المثال:
هذا لا يعبّر، مع ذلك، عن مفهوم “إعادة الكتابة”، أي عندما يتغيّر جزء كبير من الملف في أي commit. ويتطلب ذلك استعلامًا أكثر تعقيدًا. وإذا اعتبرنا أن إعادة الكتابة تحدث عندما يُحذف أكثر من 50% من الملف ويُضاف 50% منه، فيمكنك تعديل الاستعلام وفقًا لتفسيرك الخاص لما يُعدّ كذلك. يقتصر الاستعلام على الملفات الحالية فقط. ندرج جميع تغييرات الملفات من خلال التجميع حسب path وcommit_hash، مع إرجاع عدد الأسطر المضافة والمحذوفة. وباستخدام window function، نقدّر الحجم الإجمالي للملف في أي لحظة زمنية عبر إجراء مجموع تراكمي، وتقدير أثر أي تغيير في حجم الملف على أنه lines added - lines removed. واستنادًا إلى هذا المقياس، يمكننا حساب النسبة المئوية من الملف التي أُضيفت أو أُزيلت في كل تغيير. وأخيرًا، نحصي عدد تغييرات الملفات التي تُعدّ إعادة كتابة لكل ملف، أي (percent_add >= 0.5) AND (percent_delete >= 0.5) AND current_size > 50. لاحظ أننا نشترط أن يزيد حجم الملفات على 50 سطرًا لتجنب احتساب المساهمات المبكرة في الملف على أنها إعادة كتابة. كما يجنّب ذلك الانحياز نحو الملفات الصغيرة جدًا، التي قد تكون أكثر عرضة لإعادة الكتابة. تشغيل

في أي يوم من أيام الأسبوع تكون احتمالية بقاء الشيفرة في المستودع هي الأعلى؟

لهذا، نحتاج إلى تمييز سطر من الشيفرة تمييزًا فريدًا. ونقدّر ذلك (إذ قد يظهر السطر نفسه عدة مرات في الملف) باستخدام المسار ومحتوى السطر. ننفّذ استعلامًا على الأسطر المضافة، ثم نربطه بالأسطر المحذوفة، مع تصفية الحالات التي يكون فيها وقوع الثانية أحدث من الأولى. ويعطينا هذا الأسطر المحذوفة التي يمكننا من خلالها حساب المدة الزمنية بين هذين الحدثين. أخيرًا، نُجري تجميعًا على مجموعة البيانات هذه لحساب متوسط عدد الأيام التي تبقى فيها الأسطر في المستودع بحسب يوم الأسبوع. تشغيل

الملفات المرتبة حسب متوسط عمر الشيفرة

يستخدم هذا الاستعلام المبدأ نفسه كما في في أي يوم من أيام الأسبوع تكون فرصة بقاء الشيفرة في المستودع هي الأعلى، وذلك من خلال السعي إلى تمييز سطر من الشيفرة بشكل فريد باستخدام مسار ومحتوى السطر. ويتيح لنا ذلك تحديد المدة الزمنية بين وقت إضافة السطر ووقت إزالته. لكننا نقتصر هنا على الملفات الحالية وعلى الشيفرة فقط، ثم نحسب متوسط هذه المدة لكل ملف على مستوى الأسطر. تشغيل

من يميل إلى كتابة المزيد من الاختبارات / شيفرة CPP / التعليقات؟

هناك عدة طرق يمكننا من خلالها تناول هذا السؤال. وإذا ركزنا على نسبة الشيفرة إلى الاختبارات، فسيكون هذا الاستعلام بسيطًا نسبيًا: إذ يحسب عدد المساهمات في المجلدات التي تحتوي على tests، ثم يحسب نسبتها إلى إجمالي المساهمات. نقصر هنا الاستعلام على المستخدمين الذين لديهم أكثر من 20 تغييرًا، للتركيز على المساهمين المنتظمين وتجنب الانحياز إلى المساهمات العابرة. play
يمكننا عرض هذا التوزيع في صورة مدرج تكراري. play
يكتب معظم المساهمين شيفرةً أكثر من الاختبارات، كما هو متوقع. فمَن يضيف أكبر عدد من التعليقات عند المساهمة في الشيفرة؟ تشغيل
لاحظ أننا نفرز حسب المساهمات البرمجية. ومن اللافت أن هذه النسبة مرتفعة لدى جميع أكبر المساهمين لدينا، وهو ما يفسّر جزئيًا سهولة قراءة شيفرتنا البرمجية.

كيف تتغير عمليات commit الخاصة بمؤلف ما بمرور الوقت من حيث النسبة المئوية للكود/التعليقات؟

ويُعدّ حساب ذلك حسب المؤلف أمرًا بسيطًا،
لكن من الناحية المثالية، نريد أن نرى كيف يتغير ذلك بصورة مُجمّعة لدى جميع المؤلفين منذ اليوم الأول الذي يبدأون فيه بإجراء commit. هل يقلّ عدد التعليقات التي يكتبونها تدريجيًا؟ لحساب ذلك، نحسب أولًا نسبة التعليقات لكل مؤلف بمرور الوقت، على غرار من الذي يميل إلى كتابة المزيد من الاختبارات / شيفرة CPP / التعليقات؟. ثم نربط هذه النسبة بتاريخ البدء الخاص بكل مؤلف، ما يتيح لنا حساب نسبة التعليقات بحسب إزاحة الأسابيع. بعد حساب متوسط إزاحة الأسابيع على مستوى جميع المؤلفين، نأخذ sample من هذه النتائج باختيار كل أسبوع عاشر. تشغيل
ومن المشجّع أن نسبة التعليقات لدينا ثابتة تقريبًا، ولا تنخفض مع طول فترة مساهمة المؤلفين.

ما متوسط المدة قبل إعادة كتابة الشيفرة، وما الوسيط (نصف عمر تآكل الشيفرة)؟

يمكننا استخدام المبدأ نفسه كما في List files that were rewritten most number of time or by most of authors لتحديد عمليات إعادة الكتابة، ولكن مع احتساب جميع الملفات. تُستخدم دالة نافذة لحساب المدة بين عمليات إعادة الكتابة لكل ملف. ومن ثم يمكننا حساب المتوسط والوسيط عبر جميع الملفات. تشغيل

ما أسوأ وقت لكتابة الشيفرة من حيث ارتفاع احتمال إعادة كتابتها؟

على غرار ما متوسط المدة قبل أن تُعاد كتابة الشيفرة وما الوسيط (نصف عمر تآكل الشيفرة)؟ وسرد الملفات التي أُعيدت كتابتها أكبر عدد من المرات أو بواسطة أكبر عدد من المؤلفين، لكننا هنا نُجمِّع حسب يوم الأسبوع. عدّل ذلك حسب الحاجة، مثلًا إلى شهر السنة. تشغيل

أيّ المؤلفين يظل شيفرتهم ثابتًا لأطول مدة؟

نعرّف «الثبات» بأنه المدة التي تبقى فيها شيفرة المؤلف قبل أن تُعاد كتابته. وعلى غرار السؤال السابق ما متوسط الوقت قبل إعادة كتابة الشيفرة، وما الوسيط (نصف عمر تآكل الشيفرة)؟، نستخدم المقياس نفسه لعمليات إعادة الكتابة، أي 50% إضافات و50% عمليات حذف في الملف. ونحسب متوسط وقت إعادة الكتابة لكل مؤلف، ولا نأخذ في الاعتبار إلا المساهمين الذين لديهم أكثر من ملفين. تشغيل

أطول سلسلة من أيام الـcommit المتتالية حسب المؤلف

يتطلب هذا الاستعلام أولًا حساب الأيام التي أجرى فيها المؤلف عمليات commit. وباستخدام دالة نافذة، مع partitioning حسب المؤلف، يمكننا حساب عدد الأيام بين عمليات الـcommit الخاصة به. ولكل commit، إذا كان الفاصل الزمني منذ آخر commit يومًا واحدًا، نعدّه متتاليًا (1)، وإلا فتكون القيمة 0، مع تخزين هذه النتيجة في consecutive_day. بعد ذلك، تحسب array functions أطول sequence من القيم 1 المتتالية لكل مؤلف. أولًا، تُستخدم الدالة groupArray لتجميع جميع قيم consecutive_day الخاصة بالمؤلف. ثم يُقسَّم هذا Array من القيم 1 و0 عند القيم 0 إلى Arrays فرعية. وأخيرًا، نحسب أطول Array فرعي. تشغيل

سجل الالتزامات لكل سطر في ملف

يمكن إعادة تسمية الملفات. وعند حدوث ذلك، نحصل على حدث إعادة تسمية، حيث تُضبط قيمة العمود path على المسار الجديد للملف، بينما يمثّل old_path الموقع السابق، على سبيل المثال. تشغيل
هذا يجعل عرض السجل الكامل لملفٍ ما أمرًا صعبًا، لأننا لا نملك قيمة واحدة تربط جميع التغييرات على الأسطر أو الملفات. لمعالجة ذلك، يمكننا استخدام User Defined Functions (UDFs). ولا يمكن لهذه الدوال، في الوقت الحالي، أن تكون تكرارية، لذا لتحديد سجل ملفٍ ما يجب علينا تعريف سلسلة من UDFs تستدعي بعضها بعضًا بشكل صريح. وهذا يعني أنه لا يمكننا تتبّع عمليات إعادة التسمية إلا حتى حدّ أقصى من التعمّق — والمثال أدناه يصل إلى 5 مستويات. ومن غير المرجّح أن يُعاد تسمية ملفٍ ما مرات أكثر من ذلك، لذا فهذا كافٍ في الوقت الحالي.
عند استدعاء file_path_history('src/Storages/StorageReplicatedMergeTree.cpp')، نتتبّع سجل إعادة التسمية بصورة تكرارية، بحيث يستدعي كل استدعاء للدالة المستوى التالي باستخدام old_path. وتُدمَج النتائج باستخدام arrayConcat. على سبيل المثال،
يمكننا الآن استخدام هذه الإمكانية لتجميع عمليات commit عبر السجل الكامل لملف. في هذا المثال، نعرض عملية commit واحدة لكل قيمة من قيم path.

أسئلة عالقة

Git blame

يصعب هنا بشكل خاص الوصول إلى نتيجة دقيقة، نظرًا لعدم إمكانية الاحتفاظ بالحالة حاليًا داخل array functions. وسيصبح ذلك ممكنًا باستخدام arrayFold أو arrayReduce، ما يتيح الاحتفاظ بالحالة في كل تكرار. وقد يبدو الحل التقريبي التالي، وهو كافٍ لتحليل عام عالي المستوى، كما يلي:
نرحّب هنا بالحلول الدقيقة والمحسَّنة.
آخر تعديل في ٢ يوليو ٢٠٢٦