Following up with this, I have a bunch of coordinates and I draw them on a bitmap image as a coordinate system. Now, I would like to get rid of all the noise, and filter coordinates to give a "clearer" or "cleaner" path and "less" or "better" data to work on. To explain more, I will need to expose my awesome painting skills as follows:
Current:
Desired:
Notice:
I will need to delete coordinates
I might need to add coordinates
I might need to ignore shortest neighbor in some cases
The only thing I can think of, is to use a shortest path algorithm such as A* and Dijkstra. And populate data in some sort of data structure to contain neighbors and costs for every node and then to execute the algorithm. I don't want to start something that might be wrong or waste. I would love to see a pseudo code if possible on how could I solve such a problem?
P.S I am currently on Wpf C# but I am open to use C# or C++ for any task. Thanks
You are looking for an operation called thinning or skeletonization, possibly followed by some post-processing to remove small components. There are different algorithms for this that offer different properties. For example Guo and Hall's and Zhang and Suen's.
What you're after is a path finding application. There are several ways to approach this, but one of the simpler ways is to:
Pick a starting point, add to list
While True:
For each border_pt bordering last point on list:
Count number of points bordering border_pt
If count > best_count:
Mark border_pt as best
if border_pt is empty:
break
Add border_pt to list
Here's some C# code that does just that, it generates a simple list based on your cloud:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace WindowsFormsApplication1
{
class ExampleProgram : Form
{
const int GridWidth = 24;
const int GridHeight = 15;
List<Point> m_points = new List<Point>();
List<Point> m_trail = new List<Point>();
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new ExampleProgram());
}
ExampleProgram()
{
// Simple little tool to add a bunch of points
AddPoints(
0, 4, 1, 3, 1, 4, 1, 5, 2, 4, 2, 5, 2, 6, 3, 4, 3, 5, 4, 5, 4, 6, 5, 5, 6, 5,
6, 4, 5, 4, 7, 4, 7, 3, 8, 3, 8, 4, 8, 5, 8, 6, 9, 6, 9, 5, 9, 4, 9, 3, 10, 2,
10, 3, 10, 4, 10, 5, 10, 6, 11, 5, 11, 4, 11, 3, 11, 2, 12, 4, 12, 5, 13, 5,
13, 6, 13, 8, 14, 8, 14, 7, 14, 6, 15, 7, 15, 8, 15, 9, 14, 9, 14, 10, 13, 10,
12, 10, 11, 10, 13, 11, 14, 11, 15, 11, 15, 12, 16, 12, 17, 12, 18, 12, 19,
12, 18, 11, 17, 11, 17, 10, 18, 10, 19, 10, 19, 9, 19, 8, 20, 8, 21, 8, 18,
7, 19, 7, 20, 7, 21, 7, 21, 6, 22, 6, 23, 6, 21, 5, 20, 5, 19, 5, 19, 4, 18,
4, 17, 4, 20, 3, 21, 3, 22, 3, 20, 2, 19, 2, 18, 2, 19, 1, 20, 1, 21, 1, 19,
0, 18, 0, 10, 0, 4, 1);
// Very basic form logic
ClientSize = new System.Drawing.Size(GridWidth * 20, GridHeight * 20);
DoubleBuffered = true;
Paint += ExampleProgram_Paint;
// Add a new point to the form (commented out)
// MouseUp += ExampleProgram_MouseUp_AddPoint;
// Draw the trail we find
MouseUp += ExampleProgram_MouseUp_AddTrail;
// Pick a starting point to start finding the trail from
// TODO: Left as an excersize for the reader to decide how to pick
// the starting point programatically
m_trail.Add(new Point(0, 4));
}
IEnumerable<Point> Border(Point pt)
{
// Return all points that border a give point
if (pt.X > 0)
{
if (pt.Y > 0)
{
yield return new Point(pt.X - 1, pt.Y - 1);
}
yield return new Point(pt.X - 1, pt.Y);
if (pt.Y < GridHeight - 1)
{
yield return new Point(pt.X - 1, pt.Y + 1);
}
}
if (pt.Y > 0)
{
yield return new Point(pt.X, pt.Y - 1);
}
if (pt.Y < GridHeight - 1)
{
yield return new Point(pt.X, pt.Y + 1);
}
if (pt.X < GridWidth - 1)
{
if (pt.Y > 0)
{
yield return new Point(pt.X + 1, pt.Y - 1);
}
yield return new Point(pt.X + 1, pt.Y);
if (pt.Y < GridHeight - 1)
{
yield return new Point(pt.X + 1, pt.Y + 1);
}
}
}
void AddPoints(params int[] points)
{
// Helper to add a bunch of points to our list of points
for (int i = 0; i < points.Length; i += 2)
{
m_points.Add(new Point(points[i], points[i + 1]));
}
}
void ExampleProgram_MouseUp_AddTrail(object sender, MouseEventArgs e)
{
// Calculate the trail
while (true)
{
// Find the best point for the next point
int bestCount = 0;
Point best = new Point();
// At the current end point, test all the points around it
foreach (var pt in Border(m_trail[m_trail.Count - 1]))
{
// And for each point, see how many points this point borders
int count = 0;
if (m_points.Contains(pt) && !m_trail.Contains(pt))
{
foreach (var test in Border(pt))
{
if (m_points.Contains(test))
{
if (m_trail.Contains(test))
{
// This is a point both in the original cloud, and the current
// trail, so give it a negative weight
count--;
}
else
{
// We haven't visited this point, so give it a positive weight
count++;
}
}
}
}
if (count > bestCount)
{
// This point looks better than anything we've found, so
// it's the best one so far
bestCount = count;
best = pt;
}
}
if (bestCount <= 0)
{
// We either didn't find anything, or what we did find was bad, so
// break out of the loop, we're done
break;
}
m_trail.Add(best);
}
Invalidate();
}
void ExampleProgram_MouseUp_AddPoint(object sender, MouseEventArgs e)
{
// Just add the point, and dump it out
int x = (int)Math.Round((((double)e.X) - 10.0) / 20.0, 0);
int y = (int)Math.Round((((double)e.Y) - 10.0) / 20.0, 0);
m_points.Add(new Point(x, y));
Debug.WriteLine("m_points.Add(new Point(" + x + ", " + y + "));");
Invalidate();
}
void ExampleProgram_Paint(object sender, PaintEventArgs e)
{
// Simple drawing, just draw a grid, and the points
e.Graphics.Clear(Color.White);
for (int x = 0; x < GridWidth; x++)
{
e.Graphics.DrawLine(Pens.Black, x * 20 + 10, 0, x * 20 + 10, ClientSize.Height);
}
for (int y = 0; y < GridHeight; y++)
{
e.Graphics.DrawLine(Pens.Black, 0, y * 20 + 10, ClientSize.Width, y * 20 + 10);
}
foreach (var pt in m_points)
{
e.Graphics.FillEllipse(Brushes.Black, (pt.X * 20 + 10) - 5, (pt.Y * 20 + 10) - 5, 10, 10);
}
foreach (var pt in m_trail)
{
e.Graphics.FillEllipse(Brushes.Red, (pt.X * 20 + 10) - 6, (pt.Y * 20 + 10) - 6, 12, 12);
}
}
}
}
You might want to consider treating your coordinates as a binary image and apply some Morphological techniques to the image.
Thinning might give you good results, but processing like this can be tricky to get working well in a wide range of cases.
I'm building an application that will tell your Chinese sign. I looked around but only found charts (from 1900 to 2020), and no logic to create something more dynamic.
Is there no logic for determining a Chinese zodiac?
Does that answer your question:
public string ChineseZodiac(System.DateTime date)
{
System.Globalization.EastAsianLunisolarCalendar cc =
new System.Globalization.ChineseLunisolarCalendar();
int sexagenaryYear = cc.GetSexagenaryYear(date);
int terrestrialBranch = cc.GetTerrestrialBranch(sexagenaryYear);
// string[] years = "rat,ox,tiger,hare,dragon,snake,horse,sheep,monkey,fowl,dog,pig".Split(',');
// string[] years = "Rat,Ox,Tiger,Rabbit,Dragon,Snake,Horse,Goat,Monkey,Rooster,Dog,Pig".Split(',');
// string[] years = new string[]{ "rat", "ox", "tiger", "hare", "dragon", "snake", "horse", "sheep", "monkey", "fowl", "dog", "pig" };
string[] years = new string[]{ "Rat", "Ox", "Tiger", "Rabbit", "Dragon", "Snake", "Horse", "Goat", "Monkey", "Rooster", "Dog", "Pig" };
return years[terrestrialBranch - 1];
} // End Function ChineseZodiac
Fore those that need the source code for another programming language:
I've ripped the corresponding sources out of .NET Framwork, here:
https://gist.github.com/ststeiger/709354299a457e2d79b06d0127096fee
Edit:
I ported this to JavaScript:
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ChineseZodiac = void 0;
var DateTimeKind;
(function (DateTimeKind) {
DateTimeKind[DateTimeKind["Unspecified"] = 0] = "Unspecified";
DateTimeKind[DateTimeKind["Utc"] = 1] = "Utc";
DateTimeKind[DateTimeKind["Local"] = 2] = "Local";
})(DateTimeKind || (DateTimeKind = {}));
var GregorianCalendar = (function () {
function GregorianCalendar() {
}
GregorianCalendar.prototype.GetYear = function (time) {
var ticks = time.JavaScriptTicks;
var dt = new Date(ticks);
return dt.getUTCFullYear();
};
GregorianCalendar.prototype.GetMonth = function (time) {
var ticks = time.JavaScriptTicks;
var dt = new Date(ticks);
return (dt.getUTCMonth() + 1);
};
GregorianCalendar.prototype.GetDayOfMonth = function (time) {
var ticks = time.JavaScriptTicks;
var dt = new Date(ticks);
return dt.getUTCDate();
};
return GregorianCalendar;
}());
var DotNetDateTime = (function () {
function DotNetDateTime(ticks, dt) {
this.m_ticks = 0;
this.m_ticks = ticks;
}
DotNetDateTime.fromJsDate = function (dt) {
var jsTicks = dt.getTime();
var dotNetJsbaseTicks = 621355968000000000;
var tenK = 10000;
var dotTicks = dotNetJsbaseTicks + jsTicks * tenK;
return new DotNetDateTime(dotTicks, DateTimeKind.Unspecified);
};
Object.defineProperty(DotNetDateTime.prototype, "Ticks", {
get: function () {
return this.m_ticks;
},
enumerable: false,
configurable: true
});
Object.defineProperty(DotNetDateTime.prototype, "JavaScriptTicks", {
get: function () {
var dotNetJsbaseTicks = 621355968000000000;
var dotNetTicksSince1970 = this.m_ticks - dotNetJsbaseTicks;
var jsTicks = parseInt((dotNetTicksSince1970 / 10000).toString(), 10);
return jsTicks;
},
enumerable: false,
configurable: true
});
return DotNetDateTime;
}());
var MinLunisolarYear = 1901;
var MaxLunisolarYear = 2100;
var DaysPerYear = 365;
var DaysPer4Years = DaysPerYear * 4 + 1;
var DaysPer100Years = DaysPer4Years * 25 - 1;
var DaysPer400Years = DaysPer100Years * 4 + 1;
var DaysTo10000 = DaysPer400Years * 25 - 366;
var TicksPerMillisecond = 10000;
var TicksPerSecond = TicksPerMillisecond * 1000;
var TicksPerMinute = TicksPerSecond * 60;
var TicksPerHour = TicksPerMinute * 60;
var TicksPerDay = TicksPerHour * 24;
var MinTicks = 0;
var MaxTicks = DaysTo10000 * TicksPerDay - 1;
var MinValue = new DotNetDateTime(MinTicks, DateTimeKind.Unspecified);
var MaxValue = new DotNetDateTime(MaxTicks, DateTimeKind.Unspecified);
var Jan1Month = 1;
var Jan1Date = 2;
var nDaysPerMonth = 3;
var s_daysToMonth365 = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];
var s_daysToMonth366 = [0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335];
var s_yinfo = [
[0, 2, 19, 19168],
[0, 2, 8, 42352],
[5, 1, 29, 21096],
[0, 2, 16, 53856],
[0, 2, 4, 55632],
[4, 1, 25, 27304],
[0, 2, 13, 22176],
[0, 2, 2, 39632],
[2, 1, 22, 19176],
[0, 2, 10, 19168],
[6, 1, 30, 42200],
[0, 2, 18, 42192],
[0, 2, 6, 53840],
[5, 1, 26, 54568],
[0, 2, 14, 46400],
[0, 2, 3, 54944],
[2, 1, 23, 38608],
[0, 2, 11, 38320],
[7, 2, 1, 18872],
[0, 2, 20, 18800],
[0, 2, 8, 42160],
[5, 1, 28, 45656],
[0, 2, 16, 27216],
[0, 2, 5, 27968],
[4, 1, 24, 44456],
[0, 2, 13, 11104],
[0, 2, 2, 38256],
[2, 1, 23, 18808],
[0, 2, 10, 18800],
[6, 1, 30, 25776],
[0, 2, 17, 54432],
[0, 2, 6, 59984],
[5, 1, 26, 27976],
[0, 2, 14, 23248],
[0, 2, 4, 11104],
[3, 1, 24, 37744],
[0, 2, 11, 37600],
[7, 1, 31, 51560],
[0, 2, 19, 51536],
[0, 2, 8, 54432],
[6, 1, 27, 55888],
[0, 2, 15, 46416],
[0, 2, 5, 22176],
[4, 1, 25, 43736],
[0, 2, 13, 9680],
[0, 2, 2, 37584],
[2, 1, 22, 51544],
[0, 2, 10, 43344],
[7, 1, 29, 46248],
[0, 2, 17, 27808],
[0, 2, 6, 46416],
[5, 1, 27, 21928],
[0, 2, 14, 19872],
[0, 2, 3, 42416],
[3, 1, 24, 21176],
[0, 2, 12, 21168],
[8, 1, 31, 43344],
[0, 2, 18, 59728],
[0, 2, 8, 27296],
[6, 1, 28, 44368],
[0, 2, 15, 43856],
[0, 2, 5, 19296],
[4, 1, 25, 42352],
[0, 2, 13, 42352],
[0, 2, 2, 21088],
[3, 1, 21, 59696],
[0, 2, 9, 55632],
[7, 1, 30, 23208],
[0, 2, 17, 22176],
[0, 2, 6, 38608],
[5, 1, 27, 19176],
[0, 2, 15, 19152],
[0, 2, 3, 42192],
[4, 1, 23, 53864],
[0, 2, 11, 53840],
[8, 1, 31, 54568],
[0, 2, 18, 46400],
[0, 2, 7, 46752],
[6, 1, 28, 38608],
[0, 2, 16, 38320],
[0, 2, 5, 18864],
[4, 1, 25, 42168],
[0, 2, 13, 42160],
[10, 2, 2, 45656],
[0, 2, 20, 27216],
[0, 2, 9, 27968],
[6, 1, 29, 44448],
[0, 2, 17, 43872],
[0, 2, 6, 38256],
[5, 1, 27, 18808],
[0, 2, 15, 18800],
[0, 2, 4, 25776],
[3, 1, 23, 27216],
[0, 2, 10, 59984],
[8, 1, 31, 27432],
[0, 2, 19, 23232],
[0, 2, 7, 43872],
[5, 1, 28, 37736],
[0, 2, 16, 37600],
[0, 2, 5, 51552],
[4, 1, 24, 54440],
[0, 2, 12, 54432],
[0, 2, 1, 55888],
[2, 1, 22, 23208],
[0, 2, 9, 22176],
[7, 1, 29, 43736],
[0, 2, 18, 9680],
[0, 2, 7, 37584],
[5, 1, 26, 51544],
[0, 2, 14, 43344],
[0, 2, 3, 46240],
[4, 1, 23, 46416],
[0, 2, 10, 44368],
[9, 1, 31, 21928],
[0, 2, 19, 19360],
[0, 2, 8, 42416],
[6, 1, 28, 21176],
[0, 2, 16, 21168],
[0, 2, 5, 43312],
[4, 1, 25, 29864],
[0, 2, 12, 27296],
[0, 2, 1, 44368],
[2, 1, 22, 19880],
[0, 2, 10, 19296],
[6, 1, 29, 42352],
[0, 2, 17, 42208],
[0, 2, 6, 53856],
[5, 1, 26, 59696],
[0, 2, 13, 54576],
[0, 2, 3, 23200],
[3, 1, 23, 27472],
[0, 2, 11, 38608],
[11, 1, 31, 19176],
[0, 2, 19, 19152],
[0, 2, 8, 42192],
[6, 1, 28, 53848],
[0, 2, 15, 53840],
[0, 2, 4, 54560],
[5, 1, 24, 55968],
[0, 2, 12, 46496],
[0, 2, 1, 22224],
[2, 1, 22, 19160],
[0, 2, 10, 18864],
[7, 1, 30, 42168],
[0, 2, 17, 42160],
[0, 2, 6, 43600],
[5, 1, 26, 46376],
[0, 2, 14, 27936],
[0, 2, 2, 44448],
[3, 1, 23, 21936],
[0, 2, 11, 37744],
[8, 2, 1, 18808],
[0, 2, 19, 18800],
[0, 2, 8, 25776],
[6, 1, 28, 27216],
[0, 2, 15, 59984],
[0, 2, 4, 27296],
[4, 1, 24, 43872],
[0, 2, 12, 43744],
[0, 2, 2, 37600],
[3, 1, 21, 51568],
[0, 2, 9, 51552],
[7, 1, 29, 54440],
[0, 2, 17, 54432],
[0, 2, 5, 55888],
[5, 1, 26, 23208],
[0, 2, 14, 22176],
[0, 2, 3, 42704],
[4, 1, 23, 21224],
[0, 2, 11, 21200],
[8, 1, 31, 43352],
[0, 2, 19, 43344],
[0, 2, 7, 46240],
[6, 1, 27, 46416],
[0, 2, 15, 44368],
[0, 2, 5, 21920],
[4, 1, 24, 42448],
[0, 2, 12, 42416],
[0, 2, 2, 21168],
[3, 1, 22, 43320],
[0, 2, 9, 26928],
[7, 1, 29, 29336],
[0, 2, 17, 27296],
[0, 2, 6, 44368],
[5, 1, 26, 19880],
[0, 2, 14, 19296],
[0, 2, 3, 42352],
[4, 1, 24, 21104],
[0, 2, 10, 53600],
[8, 1, 30, 59696],
[0, 2, 18, 54560],
[0, 2, 7, 55968],
[6, 1, 27, 27472],
[0, 2, 15, 22224],
[0, 2, 5, 19168],
[4, 1, 25, 42216],
[0, 2, 12, 41680],
[0, 2, 1, 53584],
[2, 1, 21, 55592],
[0, 2, 9, 54560],
];
function GregorianIsLeapYear(y) {
if ((y % 4) != 0) {
return false;
}
if ((y % 100) != 0) {
return true;
}
return (y % 400) == 0;
}
function GetYearInfo(lunarYear, index) {
if (lunarYear < MinLunisolarYear || lunarYear > MaxLunisolarYear) {
throw new Error("year");
}
return s_yinfo[lunarYear - MinLunisolarYear][index];
}
function CheckTicksRange(ticks) {
if (ticks < MinValue.Ticks || ticks > MaxValue.Ticks) {
throw new Error("time");
}
}
function GregorianToLunar(solarYear, solarMonth, solarDate) {
var outData = { lunarYear: 0, lunarMonth: 0, lunarDate: 0 };
var isLeapYear = GregorianIsLeapYear(solarYear);
var jan1Month;
var jan1Date;
var solarDay = isLeapYear ? s_daysToMonth366[solarMonth - 1] : s_daysToMonth365[solarMonth - 1];
solarDay += solarDate;
var lunarDay = solarDay;
outData.lunarYear = solarYear;
if (outData.lunarYear == (MaxLunisolarYear + 1)) {
outData.lunarYear--;
lunarDay += (GregorianIsLeapYear(outData.lunarYear) ? 366 : 365);
jan1Month = GetYearInfo(outData.lunarYear, Jan1Month);
jan1Date = GetYearInfo(outData.lunarYear, Jan1Date);
}
else {
jan1Month = GetYearInfo(outData.lunarYear, Jan1Month);
jan1Date = GetYearInfo(outData.lunarYear, Jan1Date);
if ((solarMonth < jan1Month) ||
(solarMonth == jan1Month && solarDate < jan1Date)) {
outData.lunarYear--;
lunarDay += (GregorianIsLeapYear(outData.lunarYear) ? 366 : 365);
jan1Month = GetYearInfo(outData.lunarYear, Jan1Month);
jan1Date = GetYearInfo(outData.lunarYear, Jan1Date);
}
}
lunarDay -= s_daysToMonth365[jan1Month - 1];
lunarDay -= (jan1Date - 1);
var mask = 0x8000;
var yearInfo = GetYearInfo(outData.lunarYear, nDaysPerMonth);
var days = ((yearInfo & mask) != 0) ? 30 : 29;
outData.lunarMonth = 1;
while (lunarDay > days) {
lunarDay -= days;
outData.lunarMonth++;
mask >>= 1;
days = ((yearInfo & mask) != 0) ? 30 : 29;
}
outData.lunarDate = lunarDay;
return outData;
}
function TimeToLunar(time) {
var gregorianCalendar = new GregorianCalendar();
var gy = gregorianCalendar.GetYear(time);
var gm = gregorianCalendar.GetMonth(time);
var gd = gregorianCalendar.GetDayOfMonth(time);
var ad = GregorianToLunar(gy, gm, gd);
return {
year: ad.lunarYear
};
}
function GetSexagenaryYear(time) {
CheckTicksRange(time.Ticks);
var x = TimeToLunar(time);
return ((x.year - 4) % 60) + 1;
}
function GetTerrestrialBranch(sexagenaryYear) {
if (sexagenaryYear < 1 || sexagenaryYear > 60) {
throw new Error("sexagenaryYear");
}
return ((sexagenaryYear - 1) % 12) + 1;
}
function ChineseZodiac(date) {
var dotNetDate = DotNetDateTime.fromJsDate(date);
var sexagenaryYear = GetSexagenaryYear(dotNetDate);
var terrestrialBranch = GetTerrestrialBranch(sexagenaryYear);
var years = ["Rat", "Ox", "Tiger", "Rabbit", "Dragon", "Snake", "Horse", "Goat", "Monkey", "Rooster", "Dog", "Pig"];
return years[terrestrialBranch - 1];
}
exports.ChineseZodiac = ChineseZodiac;
Testing:
var exports = {};
// [copy-pasting above code]
exports.ChineseZodiac(new Date(1970,0,1))
exports.ChineseZodiac(new Date(2021,0,1))
exports.ChineseZodiac(new Date(2022,0,1))
all returns the same values as dotnet.
By using a calculator
2013-4=2009
2009/12= 167.41666
167*12=2004
2009-2004=5 (5 is snake which was the animal for 2013)
Use this to calculate the year.
<?php
$year = 2013;
switch (($year - 4) % 12) {
case 0: $zodiac = 'Rat'; break;
case 1: $zodiac = 'Ox'; break;
case 2: $zodiac = 'Tiger'; break;
case 3: $zodiac = 'Rabbit'; break;
case 4: $zodiac = 'Dragon'; break;
case 5: $zodiac = 'Snake'; break;
case 6: $zodiac = 'Horse'; break;
case 7: $zodiac = 'Goat'; break;
case 8: $zodiac = 'Monkey'; break;
case 9: $zodiac = 'Rooster'; break;
case 10: $zodiac = 'Dog'; break;
case 11: $zodiac = 'Pig'; break;
}
echo "{$year} is the year of the {$zodiac}.<br />";
?>
Wikipedia has a reference to 2044.
http://en.wikipedia.org/wiki/Chinese_zodiac
Using Year of the Rat as an example (for years after 1984), it looks like Rat cycles every:
383, 353, 353, 383, 354 days
Notice the last cycle is 354 which is more than likely due to Leap Year. Maybe using this formula, you can work out any year up to maybe 2100 or so.
I used the following T-SQL to deduce those numbers
select DATEDIFF(D,'02/2/1984', '02/19/1985')
select DATEDIFF(D,'02/19/1996', '02/6/1997')
select DATEDIFF(D,'02/7/2008', '01/25/2009')
select DATEDIFF(D,'01/25/2020', '02/11/2021')
select DATEDIFF(D,'02/11/2032', '01/30/2033')
If you are serious about finding a non-tabular mechanism for calculating the years of the Chinese Zodiac, then I recommend looking at 'Calendrical Calculations, 3rd Edition' which has (LISP) code to handle calculations for the Chinese New Year, and from that, deducing the Year of the <relevant-animal> is straight-forward. That book covers many calendrical systems and is an interesting read. Being a luni-solar calendar, the Chinese calendar is quite complex; the mathematics gets quite detailed.
It is probably simpler, and likely more compact, code-wise, to use a table, though.
There is always a question what is quicker to test and verify.
When developing chinese zodiac calculator on calculla, we decided to use lookup table - as this was just quicker and more convenient to code, than actually testing any algo for it (even if algo may be simple, you still need time to test it).
This lookup was not a big table and you can actually get the javascript code from source of our website.
First of all you must create an array of the zodiac signs exactly as below
DECLARE sign : [array(1.....12)]
DECLARE year : INTEGER
sign(1) ← "Rat"
sign(2) ← "Ox"
sign(3) ← "Tiger"
sign(4) ← "Rabbit"
sign(5) ← "Dragon"
sign(6) ← "Snake"
sign(7) ← "Horse"
sign(8) ← "Goat"
sign(9) ← "Monkey"
sign(10) ← "Rooster"
sign(11) ← "Dog"
sign(12) ← "Pig"
DECLARE X, Y, N : INTEGER
X ← (year - 4)
Y ← (X DIV 12) // DIV return only the natural number
Y ← (Y * 12)
N ← N + 1
OUTPUT sign(N)
Public Function ChineseSign(ByVal DoB As Date) As String
Dim CSign As String = ""
Dim YearSign As New Short
YearSign = Year(DoB) Mod 12
'// Uncomment the following to use Feb 12th as New Year calculation //
'If Month(DoB) = 1 Then
'YearSign = YearSign - 1
'If YearSign = 0 Then YearSign = 12
'ElseIf Month(DoB) = 2 And DoB.Day < 12 Then
'YearSign = YearSign - 1
'If YearSign = 0 Then YearSign = 12
'End If
Select Case YearSign
Case 1
CSign = "Rooster"
Case 2
CSign = "Dog"
Case 3
CSign = "Pig (Boar)"
Case 4
CSign = "Rat"
Case 5
CSign = "Ox"
Case 6
CSign = "Tiger"
Case 7
CSign = "Rabbit"
Case 8
CSign = "Dragon"
Case 9
CSign = "Snake"
Case 10
CSign = "Horse"
Case 11
CSign = "Goat"
Case 12
CSign = "Monkey"
End Select
Select Case CSign
Case "Ox"
Return "an " & CSign
Case Else
Return "a " & CSign
End Select
End Function