Вычисляем животное китайского календаря

в 17:40, , рубрики: .net, C#, tdd, математика, метки: , , , , ,

Как определить, какое животное китайского кругового календаря соответствует любому году? Для начала, обнаружим некоторые закономерности.

Пусть М = некоему набору исторических годов:

int[] M = { 1905,1917,1941,1953,1989,2001,2013,2025,2241 };

Тогда, для него справедлив следующий тест:


//((1))
	// Набор специальных исторических годов M
	static int[] GetSpecialHistoryYears() {
		int[] M = { 1905,1917,1941,1953, 1989,2001,2013,2025,  2241 };
		return M;
	}
	
//((2))
	// Проверяем математические закономерности годов M
	[Test]
	protected void SpecialHistoryYearsMath() {
		int[] M = GetSpecialHistoryYears();
		
		// A и B - индексы массива
		// вкладывая A в B мы получаем возможность брать любую пару в массиве M.
		for( int A = 0; A < M.Length; A++ ) {
			for( int B = 0; B < M.Length; B++ ) {
				// разница любой пары в наборе кратна 12
				Assert.AreEqual( 0, Math.Abs(M[A]-M[B]) % 12, 
				"Expected 12x differrence" );
			}
		}

	}


Тест подтверждает, что любая пара чисел в наборе M имеет двенадцатикратную разницу, т.е., чтобы перейти от одного числа пары к любому другому, следующему, предыдущему, или вообще не двигаться, нам нужно совершить двенадцатикратное смещение.

Если взять круговой циферблат из 12 чисел — круговые часы — и поставить стрелку на любое число в нём от 1 до 12 — число Х, — то мы обнаружим следующую закономерность.

Каждый раз смещая стрелку от Х на +12 шагов мы будем возвращаться к начальному числу Х. Прокрутив ещё на +12, мы опять вернёмся к Х.

Т.е. правило круговых часов:

ПРАВИЛО КРУГОВЫХ ЧАСОВ

Двенадцатикратное смещение стрелки круговых часов от начальной позиции, возвращает стрелку на начальную позицию.

Продемонстрируем, как это работает в коде:


	//((3))
	public class CircularClock {
		// Стрелка круговых часов.
		public int Arrow {
			get;
			set;
		}
		
		// Создание круговых часов с установкой стрелки на заданное параметром число
		public CircularClock( int arrow ) {
			this.Arrow = arrow;
		}
	
		// Перемещение стрелки.
		public void ShiftArrowBy( int shifts ) {
			this.Arrow = GetArrowForRealValue( this.Arrow + shifts );
		}

		// для получения стрелки из любого значения нужно произвести определенные вычисления,
		// чтобы стрелка была в пределах от 1 до 12
		protected int GetArrowForRealValue( int val ) {
			if( val % 12 == 0 ) {
				return 12;
			} else if( val > 0 ) {
				return val % 12;
			} else {
				return 12 + val % 12;
			}
		}
	}
	
	
	// Тестируем смещение стрелки
	[TestCase( 10,+3, 1 )]
	[TestCase( 10,+5, 3 )]
	[TestCase( 2,+10, 12 )]
	[TestCase( 10,+24, 10 )]
	[TestCase( 2,-3, 11 )]
	[TestCase( 2,-24, 2 )]
	[TestCase( 4,-7, 9 )]
	[TestCase( 8,-26, 6 )]
	[TestCase( 8,+26, 10 )]
	protected void CClockShiftArrow( int start,int shift,int end ) {
		var clock = new CircularClock( start );

		clock.ShiftArrowBy( shift );
		
		Assert.AreEqual( end,clock.Arrow, 
		"Bad arrow after shift" );
	}

В этом коде мы проработали класс круговых часов с функцией перемещения стрелки, и протестировали, как должно работать перемещение стрелки.

Первый параметр теста — это начальное положение стрелки; второй параметр — её смещение, оно может быть таким огромным, что по модулю даже превысит лимит 12, при чём может быть как отрицательным, так и положительным; третий параметр — конечное положение стрелки, несмотря
на огромные перемещения стрелка всё равно находится в пределах от 1 до 12.

Теперь, зная правило круговых часов и закономерности набора М, поставим первое число пары M[A] на позицию X в круговых часах. Совершив двенадцатикратное смещение (M[B] — M[A]), мы доберемся до числа M[B], а стрелка вернётся на прежнее значение.

Это значит, что стрелка одинакова для любой пары и для любого элемента в наборе M.

Т.е., правило кругового циферблата для годов:

ПРАВИЛО КРУГОВОГО ЦИФЕРБЛАТА ДЛЯ ГОДОВ

Если разница пары годов двенадцатикратна, то обоим годам соответствует одна и та же стрелка на круговом циферблате из 12 чисел.

Если разница ЛЮБОЙ пары в наборе двенадцатикратна, то ВСЕМУ НАБОРУ соответствует одинаковая стрелка.

Проверим работу стрелки для годов М.

//((3.1))
	[Test]
	protected void CClockSpeciallyShiftArrow() {
		const int X = 3;
		var clock = new CircularClock( X );
		int[] M = GetSpecialHistoryYears();

		for( int A = 0; A < M.Length; A++ ) {
			for( int B = 0; B < M.Length; B++ ) {
				clock.Arrow = X;
				clock.ShiftArrowBy( M[B] - M[A] );
				
				Assert.AreEqual( X,clock.Arrow, "bad arrow for pair:{0} -> {1}",M[A],M[B] );
			}
		}

		clock.Arrow = X;
		for( int J = 1; J < M.Length; J++ ) {
			clock.ShiftArrowBy( M[J] - M[J-1] );

			Assert.AreEqual( X,clock.Arrow, "bad arrow for {0}->{1}",M[J],M[J-1] );
		}
	}

В этом тесте мы вначале устанавливаем константу X, потом создаём круговые часы, переводя стрелку на X, и загружаем года M.

Сперва мы проверяем перевод стрелки на разницу любой пары (M[B] — M[A]), чтобы в результате стрелка вернулась на то же место, так как разница двенадцатикратна.

Потом мы сбрасываем стрелку на X и проверяем поочередно перевод на разницу текущего числа и предыдущего (M[J] — M[J-1]) — стрелка опять-таки должна возвращаться на изначальное число (X).

Китайцы

Круговым циферблатом для годов является Китайский Лунный Круговой Календарь (КЛКК) — просто надо подставить числу на циферблате соответствующее животное по порядку.

Подставив, мы обнаружим правило китая для годов, справедливое для любого набора подобного М:

ПРАВИЛО КИТАЯ ДЛЯ ГОДОВ

Двенадцатикратная разница между парой годов означает, что обоим годам соответствует ОДНО И ТОЖЕ животное Китайского Лунного Кругового Календаря (КЛКК).

Если разница ЛЮБОЙ пары годов в наборе двенадцатикратна, то ВСЕ ГОДЫ В НАБОРЕ соответствуют одному животному КЛКК.

Теперь мы знаем все правила, осталось только создать класс китайского календаря и научить его вычислять животное.


	// Так как наш календарь в сущности - круговые часы, мы должны произвести наследование
	// нашего класса от класса круговых часов.
	public class ChinaCalendar : CircularClock {
		// начальное животное для вычисления животного для любого года
		public int StartAnimal {
			get;
			set;
		}
		
		// создаём китайский календарь указывая начальное животное
		public ChinaCalendar( int start )
		: base( arrow:1 ) {
			this.StartAnimal = start;
		}
		
		
		// таки наконец вычисляем животное по году!
		public int GetAnimal( int year ) {
			// наше животное - просто стрелка для года со стартовым смещением.
			return GetArrowForRealValue( StartAnimal - 1 + year );
		}
	}
	
	// проверим работу функции, вычисляющей животное
	[TestCase( 1,10, 10 )]
	[TestCase( 1,12, 12 )]
	[TestCase( 1,24, 12 )]
	[TestCase( 1,25, 1 )]
	[TestCase( 1,26, 2 )]
	[TestCase( 3,1, 3 )]
	[TestCase( 3,10, 12 )]
	[TestCase( 3,12, 2 )]
	[TestCase( 3,13, 3 )]
	[TestCase( 3,24, 2 )]
	[TestCase( 3,25, 3 )]
	protected void ChinaCalendarGetAnimal( int start,int year,int animal ) {
		var cc = new ChinaCalendar( start );

		int wasAnimal = cc.GetAnimal( year );

		Assert.AreEqual( animal,wasAnimal, 
		"Bad animal (from:{0} year:{1})",cc.StartAnimal,year );
	}

Мы создали класс, научили его вычислять животное и протестировали, как он это делает. Первый параметр определяет начальную стрелку — начальное животное для года 1. Второй параметр — год. Третий параметр — его животное.

Животные выражаются числами, так как это наиболее удобно для вычисления и тестирования.

Теперь мы умеем это делать!

Осталось только записать в журнал и консоль животные для каждого года в наборе M. Зверь должен быть одинаковым, верно?


	// Печатаем всех зверей соответствующих годам набора M.
	[Test]
	protected void PrintSpecialChinaCalendarAnimals() {
		// создаём китайский календарь с начальным зверем №10, вообще любое число можно подставить.
		var cc = new ChinaCalendar( 10 );
		// загружаем года М
		int[] M = GetSpecialHistoryYears();
		
		// для каждого года в наборе М...
		foreach( int m in M ) {
			//... печатаем год и соответствующее животное;
			WriteLine( "{0} -> animal {1}", m,cc.GetAnimal( m ) );
		}
	}
	
	// функция печати в консоль и в журнал
	void WriteLine( String lineFormat,params object[] args ) {
		string str = string.Format( lineFormat,args );
		Console.WriteLine( str );
		string write = System.IO.File.ReadAllText("log.txt") + Environment.NewLine + str;
		System.IO.File.WriteAllText( "log.txt", write );
	}	

У нас получится следующий вывод в консоль:

Вывод

1905 -> animal 6
1917 -> animal 6
1941 -> animal 6
1953 -> animal 6
1989 -> animal 6
2001 -> animal 6
2013 -> animal 6
2025 -> animal 6
2241 -> animal 6

Да, действительно, одно животное, вопрос — какое? И как вообще, правильно вычислять нужную зверюшку, а не ту, что немного правее или немного левее от действительного?

Возьмём любой год из набора M. 1941, например. Из википедии мы узнаём, что год 1941 — это год ЗМЕИ, а ей в КЛКК соответствует порядковый номер (стрелка) 6.

Исходя из этого, экспериментальным путём мы определяем, что чтобы добиться животного № 6 для годов M, нужно поставить животное №10 в качестве начала (т.е., первым годом н.э. был ПЕТУХ).

Подведём итог

В этой статье мы научились вычислять, где стоит стрелка циферблата для какого-либо числа, соответствует ли двум числам одна и та же стрелка, что первым годом н.э. был петух, и — самое главное — как вычислять животное китайского календаря для любого года (без нудного перечисления по порядку).

Полный код:

Большой-большой полный код

[TestSuite]
class Program {
static void Main(string [] args) {
}

//((1))
// Набор специальных исторических годов M
static int[] GetSpecialHistoryYears() {
int[] M = { 1905,1917,1941,1953, 1989,2001,2013,2025, 2241 };
return M;
}

//((2))
// Проверяем математические закономерности годов M
[Test]
protected void SpecialHistoryYearsMath() {
int[] M = GetSpecialHistoryYears();

// A и B - индексы массива
// вкладывая A в B мы получаем возможность брать любую пару в массиве M.
for( int A = 0; A < M.Length; A++ ) {
for( int B = 0; B < M.Length; B++ ) {
// разница любой пары в наборе кратна 12
Assert.AreEqual( 0, Math.Abs(M[A]-M[B]) % 12,
"Expected 12x differrence" );
}
}

}

//((3))
public class CircularClock {
// Стрелка круговых часов.
public int Arrow {
get;
set;
}

// Создание круговых часов с установкой стрелки на заданное параметром число
public CircularClock( int arrow ) {
this.Arrow = arrow;
}

// Перемещение стрелки.
public void ShiftArrowBy( int shifts ) {
this.Arrow = GetArrowForRealValue( this.Arrow + shifts );
}

// для получения стрелки из любого значения нужно произвести определенные вычисления,
// чтобы стрелка была в пределах от 1 до 12
protected int GetArrowForRealValue( int val ) {
if( val % 12 == 0 ) {
return 12;
} else if( val > 0 ) {
return val % 12;
} else {
return 12 + val % 12;
}
}
}

// Тестируем смещение стрелки
[TestCase( 10,+3, 1 )]
[TestCase( 10,+5, 3 )]
[TestCase( 2,+10, 12 )]
[TestCase( 10,+24, 10 )]
[TestCase( 2,-3, 11 )]
[TestCase( 2,-24, 2 )]
[TestCase( 4,-7, 9 )]
[TestCase( 8,-26, 6 )]
[TestCase( 8,+26, 10 )]
protected void CClockShiftArrow( int start,int shift,int end ) {
var clock = new CircularClock( start );

clock.ShiftArrowBy( shift );

Assert.AreEqual( end,clock.Arrow,
"Bad arrow after shift" );
}

//((3.1))
[Test]
protected void CClockSpeciallyShiftArrow() {
const int X = 3;
var clock = new CircularClock( X );
int[] M = GetSpecialHistoryYears();

for( int A = 0; A < M.Length; A++ ) {
for( int B = 0; B < M.Length; B++ ) {
clock.Arrow = X;
clock.ShiftArrowBy( M[B] - M[A] );

Assert.AreEqual( X,clock.Arrow, "bad arrow for pair:{0} -> {1}",M[A],M[B] );
}
}

clock.Arrow = X;
for( int J = 1; J < M.Length; J++ ) {
clock.ShiftArrowBy( M[J] - M[J-1] );

Assert.AreEqual( X,clock.Arrow, "bad arrow for {0}->{1}",M[J],M[J-1] );
}
}

//((4))
// Так как наш календарь в сущности - круговые часы, мы должны произвести наследование
// нашего класса от класса круговых часов.
public class ChinaCalendar : CircularClock {
// начальное животное для вычисления животного для любого года
public int StartAnimal {
get;
set;
}

// создаём китайский календарь указывая начальное животное
public ChinaCalendar( int start )
: base( arrow:1 ) {
this.StartAnimal = start;
}

// таки наконец вычисляем животное по году!
public int GetAnimal( int year ) {
// наше животное - просто стрелка для года со стартовым смещением.
return GetArrowForRealValue( StartAnimal - 1 + year );
}
}

// проверим работу функции, вычисляющей животное
[TestCase( 1,10, 10 )]
[TestCase( 1,12, 12 )]
[TestCase( 1,24, 12 )]
[TestCase( 1,25, 1 )]
[TestCase( 1,26, 2 )]
[TestCase( 3,1, 3 )]
[TestCase( 3,10, 12 )]
[TestCase( 3,12, 2 )]
[TestCase( 3,13, 3 )]
[TestCase( 3,24, 2 )]
[TestCase( 3,25, 3 )]
protected void ChinaCalendarGetAnimal( int start,int year,int animal ) {
var cc = new ChinaCalendar( start );

int wasAnimal = cc.GetAnimal( year );

Assert.AreEqual( animal,wasAnimal,
"Bad animal (from:{0} year:{1})",cc.StartAnimal,year );
}

//((4.1))
// Печатаем всех зверей соответствующих годам набора M.
[Test]
protected void PrintSpecialChinaCalendarAnimals() {
var cc = new ChinaCalendar( 10 );
int[] M = GetSpecialHistoryYears();

foreach( int m in M ) {
WriteLine( "{0} -> animal {1}", m,cc.GetAnimal( m ) );
}
}

[Test]
protected void PrintMoreChinaCalendarAnimals() {
var cc = new ChinaCalendar( 10 );

foreach( int y in new int[]{ 1,2,3,4,5,6,7,8,9,10,11,12 } ) {
WriteLine( "{0} -> animal {1}", y,cc.GetAnimal(y) );
}
}

// функция печати в консоль и в журнал
void WriteLine( String lineFormat,params object[] args ) {
string str = string.Format( lineFormat,args );
Console.WriteLine( str );
string write = System.IO.File.ReadAllText("log.txt") + Environment.NewLine + str;
System.IO.File.WriteAllText( "log.txt", write );
}

}

Автор: бро

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js