المصروفات

المصروفات

تمرين تعلّمي

مقدمة

في Go، الدوال قيم من الدرجة الأولى. هذا يعني أنه يمكنك أن تفعل بالدوال نفس الأشياء التي يمكنك فعلها مع بقية القيم: إسناد الدوال إلى المتغيرات، وتمريرها كوسائط إلى دوال أخرى، أو حتى إرجاع دوال من دوال أخرى.

في ما يلي ننشئ دالتين، engGreeting وespGreeting، ونُسندهما إلى المتغير greeting:

import "fmt"

func engGreeting(name string) string {
	return fmt.Sprintf("Hello %s, nice to meet you!", name)
}

func espGreeting(name string) string {
	return fmt.Sprintf("¡Hola %s, mucho gusto!", name)
}

greeting := engGreeting			// greeting is a variable of type func(string) string
fmt.Println(greeting("Alice"))	// Hello Alice, nice to meet you!

greeting = espGreeting
fmt.Println(greeting("Alice")) 	// ¡Hola Alice, mucho gusto!

تتيح لنا قيم الدوال إمكانية تمرير المعاملات إلى الدوال، ليس بالبيانات فحسب، بل بالسلوك أيضًا. في المثال التالي، نمرّر السلوك إلى دالة dialog عبر المعامل greetingFunc:

func dialog(name string, greetingFunc func(string) string) {
	fmt.Println(greetingFunc(name))
	fmt.Println("I'm a dialog bot.")
}

func espGreeting(name string) string {
	return fmt.Sprintf("¡Hola %s, mucho gusto!", name)
}

greeting := espGreeting
dialog("Alice", greeting)
// =>
// ¡Hola Alice, mucho gusto!
// I'm a dialog bot.

قيمة متغير غير مُهيّأ من نوع دالة هي nil. لذلك، فإن استدعاء قيمة دالة تساوي nil يؤدي إلى panic.

var dutchGreeting func(string) string
dutchGreeting("Alice") // panic: call of nil function

يمكن مقارنة قيم الدوال بـ nil. وقد يكون هذا مفيدًا لتجنّب حالات panic غير الضرورية في البرنامج.

var dutchGreeting func(string) string
if dutchGreeting != nil {
	dutchGreeting("Alice") // safe to call dutchGreeting
}

أنواع الدوال

استخدام قيم الدوال ممكن بفضل أنواع الدوال في Go. نوع الدالة يحدّد مجموعة كل الدوال التي لها نفس تسلسل أنواع المعاملات ونفس تسلسل أنواع النتائج. ويمكن تعريف أنواع يُنشئها المستخدم فوق أنواع الدوال. على سبيل المثال، يمكن تحديث دالة dialog من الأمثلة السابقة كما يلي:

type greetingFunc func(string) string

func dialog(name string, f greetingFunc) {
	fmt.Println(f(name))
	fmt.Println("I'm a dialog bot.")
}

الدوال المجهولة

من الأدوات القوية الأخرى المتاحة بفضل دعم الدوال كقيم من الدرجة الأولى: الدوال المجهولة. تُعرَّف الدوال المجهولة عند موضع استخدامها، دون اسم يتبع الكلمة المفتاحية func. ولهذه الدوال وصول إلى متغيرات الدالة المحيطة.

على سبيل المثال:

func fib() func() int {
	var n1, n2 int

	return func() int {
		if n1 == 0 && n2 == 0 {
			n1 = 1
		} else {
			n1, n2 = n2, n1 + n2
		}
		return n2
	}
}

next := fib()
for i := range N {
  fmt.Printf("F%d\t= %4d\n", i, next())
}

يُعرّف استدعاء fib المتغيرين n1 وn2 ويُرجع دالة مجهولة، وهذه الدالة بدورها تغيّر قيم هذين المتغيرين في كل مرة تُستدعى فيها. الاستدعاء رقم N للدالة المجهولة يُرجع العدد رقم N من متتالية فيبوناتشي بدءًا من 0. وللدالة المجهولة الداخلية وصول إلى المتغيرين المحليين (n1 وn2) في الدالة المحيطة fib. وهذه طريقة رائعة لجعل قيم الدوال تحتفظ بحالتها بين الاستدعاءات. نقول إن الدالة المجهولة إغلاق للمتغيرين n1 وn2. وتُستخدم الإغلاقات على نطاق واسع في البرمجة، وقد ترى لغات أخرى تدعمها.

التعليمات

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

في هذا التمرين، ستبني برنامجًا يساعد بوب على إدارة نفقات عملائه وتحليلها.

يتعامل بوب مع Record وDaysPeriod.

يمثّل Record سجل نفقات يحتوي على اليوم الذي تم فيه الإنفاق، والمبلغ المنفق، وفئة النفقة.

// Record represents an expense record.
type Record struct {
	Day         int
	Amount      float64
	Category    string
}

يمثّل DaysPeriod نطاقًا من الأيام يشمل كل الأيام من اليوم From حتى اليوم To. ويشمل النطاق طرفيه.

// DaysPeriod represents a period of days.
type DaysPeriod struct {
	From int
	To   int
}

p := DaysPeriod{From: 1, To: 31}
// p represents all days from the day 1 to the day 31:
//  - days 1, 20, 16 and 31 are examples of days that are included
//    in the range of time specified by p
//  - days 50 and 40 are examples of days that are not included
//    in the range of time specified by p

1. نفّذ مرشّحًا عامًا للسجلات

يتعامل بوب مع الكثير من السجلات كل يوم، لكن ليس كلها مثيرًا للاهتمام، إذ يعتمد ذلك على التحليل الذي يجريه بوب. لنساعد بوب على إجراء بعض التصفية الأساسية للسجلات.

نفّذ الدالة العامة Filter لتصفية السجلات وفق معيار تعطيه دالة. تستقبل دالة التصفية هذه مجموعة من السجلات ودالة شرطية، وتُرجع فقط السجلات في المجموعة التي تحقق الشرط.

records := []Record{
  {Day: 1, Amount: 15, Category: "groceries"},
  {Day: 11, Amount: 300, Category: "utility-bills"},
  {Day: 12, Amount: 28, Category: "groceries"},
}

// Day1Records only returns true for records that are from day 1
func Day1Records(r Record) bool {
  return r.Day == 1
}

Filter(records, Day1Records)
// =>
// [
//   {Day: 1, Amount: 15, Category: "groceries"}
// ]

2. صفِّ السجلات ضمن فترة زمنية

يحتاج بوب كثيرًا إلى تصفية السجلات الواقعة في فترة زمنية معيّنة.

نفّذ الدالة ByDaysPeriod التي ستساعد بوب على إنشاء مرشّحات كهذه. تستقبل هذه الدالة DaysPeriod وتُرجع دالة تأخذ سجلًا وتخبرنا ما إذا كان السجل ضمن الفترة الزمنية التي يحددها DaysPeriod المُمرَّر كوسيط.

records := []Record{
  {Day: 1, Amount: 15, Category: "groceries"},
  {Day: 11, Amount: 300, Category: "utility-bills"},
  {Day: 12, Amount: 28, Category: "groceries"},
  {Day: 26, Amount: 300, Category: "university"},
  {Day: 28, Amount: 1300, Category: "rent"},
}

period := DaysPeriod{From: 1, To: 15}

Filter(records, ByDaysPeriod(period))
// =>
// [
//   {Day: 1, Amount: 15, Category: "groceries"},
//   {Day: 11, Amount: 300, Category: "utility-bills"},
//   {Day: 12, Amount: 28, Category: "groceries"},
// ]

3. صفِّ السجلات حسب الفئة

إلى جانب تصفية السجلات حسب فترة زمنية، يحتاج بوب أيضًا إلى تصفية السجلات حسب فئتها.

نفّذ الدالة ByCategory التي ستساعد بوب على إنشاء مرشّحات كهذه. تستقبل هذه الدالة فئة وتُرجع دالة تأخذ سجلًا وتخبرنا ما إذا كانت فئة هذا السجل مطابقة للفئة المُمرَّرة كوسيط.

records := []Record{
  {Day: 1, Amount: 15, Category: "groceries"},
  {Day: 11, Amount: 300, Category: "utility-bills"},
  {Day: 12, Amount: 28, Category: "groceries"},
  {Day: 28, Amount: 1300, Category: "rent"},
}

Filter(records, ByCategory("groceries"))
// =>
// [
//   {Day: 1, Amount: 15, Category: "groceries"},
//   {Day: 12, Amount: 28, Category: "groceries"},
// ]

4. احسب المبلغ الإجمالي للنفقات في فترة

يريد بوب أيضًا معرفة المبلغ الإجمالي للنفقات في فترة زمنية.

نفّذ الدالة TotalByPeriod لتُرجع مجموع النفقات في فترة الأيام.

records := []Record{
  {Day: 15, Amount: 16, Category: "entertainment"},
  {Day: 32, Amount: 20, Category: "groceries"},
  {Day: 40, Amount: 30, Category: "entertainment"}
}

p1 := DaysPeriod{From: 1, To: 30}
p2 := DaysPeriod{From: 31, To: 60}

TotalByPeriod(records, p1)
// => 16

TotalByPeriod(records, p2)
// => 50

5. احسب إجمالي النفقات لسجلات فئة معيّنة في فترة

في أكثر التقارير تعقيدًا التي يقدّمها بوب لعملائه، يحتاج بوب إلى تصفية السجلات حسب الفئة والفترة الزمنية في الوقت نفسه. أي أن بوب يريد معرفة إجمالي النفقات لسجلات فئة معيّنة في فترة زمنية معيّنة.

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

p1 := DaysPeriod{From: 1, To: 30}
p2 := DaysPeriod{From: 31, To: 60}

records := []Record{
  {Day: 1, Amount: 15, Category: "groceries"},
  {Day: 11, Amount: 300, Category: "utility-bills"},
  {Day: 12, Amount: 28, Category: "groceries"},
  {Day: 26, Amount: 300, Category: "university"},
  {Day: 28, Amount: 1300, Category: "rent"},
}

CategoryExpenses(records, p1, "entertainment")
// => 0, error(unknown category entertainment)

CategoryExpenses(records, p1, "rent")
// => 1300, nil

CategoryExpenses(records, p2, "rent")
// => 0, nil
تعديل عبر GitHub يفتح الرابط في نافذة أو علامة تبويب جديدة
Go Exercism

مستعد لبدء المصروفات؟

سجّل في Exercism لتتعلّم وتتقن Go عبر 34 مفهومًا165 تمرينًا، وإرشاد بشري حقيقي، وكل ذلك مجانًا.