Pull to refresh

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

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

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

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 );
}

}

Tags:
Hubs:
You can’t comment this publication because its author is not yet a full member of the community. You will be able to contact the author only after he or she has been invited by someone in the community. Until then, author’s username will be hidden by an alias.