calendar tools
authorKonrad Rosenbaum <konrad@silmor.de>
Mon, 29 May 2017 06:58:33 +0000 (08:58 +0200)
committerKonrad Rosenbaum <konrad@silmor.de>
Tue, 4 Jul 2017 21:09:43 +0000 (23:09 +0200)
Change-Id: Iee401bca1dba129923ca3524ef2a00bbb504e429

23 files changed:
.gitignore
caltools.pri [new file with mode: 0644]
caltools/caltools.pro [new file with mode: 0644]
caltools/src/easterdate.cpp [new file with mode: 0644]
caltools/src/lunar.cpp [new file with mode: 0644]
caltools/src/solar.cpp [new file with mode: 0644]
caltools/src/src.pro [new file with mode: 0644]
caltools/tests/easter/easter.pro [new file with mode: 0644]
caltools/tests/easter/test2.cpp [new file with mode: 0644]
caltools/tests/geo/geo.pro [new file with mode: 0644]
caltools/tests/geo/geotest.cpp [new file with mode: 0644]
caltools/tests/gregorian/gregorian.pro [new file with mode: 0644]
caltools/tests/gregorian/gregtest.cpp [new file with mode: 0644]
caltools/tests/lunar/lunar.cpp [new file with mode: 0644]
caltools/tests/lunar/lunar.pro [new file with mode: 0644]
caltools/tests/solar/solar.pro [new file with mode: 0644]
caltools/tests/solar/solartest.cpp [new file with mode: 0644]
include/caltools/caltools.h [new file with mode: 0644]
include/caltools/easterdate.h [new file with mode: 0644]
include/caltools/gregorian.h [new file with mode: 0644]
include/caltools/lunar.h [new file with mode: 0644]
include/caltools/solar.h [new file with mode: 0644]
taurus.pro

index b1c0c81..03660f8 100644 (file)
@@ -11,6 +11,7 @@ core.*
 *.o
 *.obj
 moc_*
+*.moc
 elam/tests/eval/evaltest*
 elam/tests/parser/parsertest*
 .qmake.stash
@@ -23,4 +24,6 @@ doc/aurora-src/
 doc/chester/
 doc/elam/source/
 doc/zip-src/
-
+caltest
+caltest.exe
+target_wrapper.sh
diff --git a/caltools.pri b/caltools.pri
new file mode 100644 (file)
index 0000000..8054fdd
--- /dev/null
@@ -0,0 +1,6 @@
+#include this into your qmake project file to
+#use the Calendar Tools library
+
+INCLUDEPATH += $$PWD/include/caltools
+LIBS += -lm -L$$PWD/lib -lcaltools
+CONFIG += link_prl
diff --git a/caltools/caltools.pro b/caltools/caltools.pro
new file mode 100644 (file)
index 0000000..db46149
--- /dev/null
@@ -0,0 +1,2 @@
+TEMPLATE=subdirs
+SUBDIRS += src tests/easter tests/gregorian tests/lunar tests/solar
diff --git a/caltools/src/easterdate.cpp b/caltools/src/easterdate.cpp
new file mode 100644 (file)
index 0000000..b7c50b0
--- /dev/null
@@ -0,0 +1,94 @@
+// Calculating Easter Dates
+//
+// (c) Konrad Rosenbaum, 2017
+// protected under the GNU LGPL v3 or at your option any newer
+
+#include <easterdate.h>
+
+// Debugger function, make if "#if 1" to enable debugging output
+#if 0
+#include <QDebug>
+#define DD(x) qDebug()<<""#x "="<<x;
+#else
+#define DD(x)
+#endif
+
+// formulas were found at http://www.dateofeaster.com/
+// some more explanations (in German): https://de.wikipedia.org/wiki/Osterzyklus#Die_Mondgleichung
+//   and: https://de.wikipedia.org/wiki/Gau%C3%9Fsche_Osterformel
+
+namespace CalTools {
+
+///calculates the date of easter for a specific year
+QDate easterSunday(quint16 year)
+{
+       DD(year);
+       const int c=year/100;           // century
+       DD(c);
+       const int q=(c-15)*3/4+10;      // gregorian correction
+       DD(q);
+       const int l=7-((year/4+year+4-q)%7); // dominical number
+       DD(l);
+       const int g=year%19+1;          // golden number
+       DD(g);
+       const int j=(g*11-10)%30;       // julian epact
+       DD(j);
+       const int s=q-10;               // solar equation
+       DD(s);
+       const int m=(c-14)*8/25;        //lunar equation
+       DD(m);
+       int e=j-s+m;
+       while(e<=0)e+=30;
+       e%=30;                          // epact
+       DD(e);
+       if(g>11 && e==25)e=26;          // second epact 25 rule
+       DD(e);
+       if(e==24)e=25;                  // epact 24 equals epact 25
+       DD(e);
+       int d;
+       if(e<24)d=44-e;                 // paschal full moon
+       else d=74-e;
+       DD(d);
+       
+       d+= 7 - ((d+10-l)%7);           // find sunday following this
+       DD(d);
+       int mo=3; //
+       if(d>61){mo=5;d-=61;}           // may
+       else if(d>31){mo=4;d-=31;}      // april
+       return QDate(year,mo,d);
+}
+
+///calculates the eastern orthodox date for Easter sunday
+QDate orthodoxEasterSunday(quint16 year)
+{
+       DD(year);
+       const int l=7-(year/4+year+4)%7; //dominical
+       DD(l);
+       const int g=year%19+1;          //gregorian
+       DD(g);
+       const int e=(g*11-11)%30;       //epact
+       DD(e);
+       const int d=(e>16)?(66-e):(36-e); // paschal
+       DD(d);
+       const int w=(d+3-l)%7;          // weekday of paschal
+       DD(w);
+       int d2=d+7-w;                   // sunday after paschal in julian calendar
+       DD(d2);
+       const int q=(year/100-15)*3/4+10; // gregorian correction
+       DD(q);
+       d2+=q;
+       DD(d2);
+       
+       int mo=3;
+       if(d2>61){mo=5;d2-=61;}
+       else if(d2>31){mo=4;d2-=31;}
+       return QDate(year,mo,d2);
+}
+
+//TODO: date of jewish pessach
+
+
+//end of namespace
+}
+
+#undef DD
diff --git a/caltools/src/lunar.cpp b/caltools/src/lunar.cpp
new file mode 100644 (file)
index 0000000..5b4b92b
--- /dev/null
@@ -0,0 +1,36 @@
+// Calculating Easter Dates
+//
+// (c) Konrad Rosenbaum, 2017
+// protected under the GNU LGPL v3 or at your option any newer
+
+#include <lunar.h>
+#include <math.h>
+
+namespace CalTools {
+
+/// amount of phases used for internal calculations
+const int LunarPhases_CycleLength=8;
+
+/// average length of a lunar cycle in solar days
+const double LunarCycleLengthInDays=29.530588853l;
+
+/// Julian day of a known new moon
+const int JulianDayJan1st1900=2415021;
+
+///returns the phase of the moon for a specific date
+/// note: this function is quite 
+LunarPhase lunarPhaseForDate(QDate d)
+{
+       //calculate where in the cycle we are (in days)
+       double phase=fmod(d.toJulianDay()-JulianDayJan1st1900+1.0, LunarCycleLengthInDays);
+       if(phase<0.0)phase+=LunarCycleLengthInDays;
+       //convert to enum base
+       int p=phase*double(LunarPhases_CycleLength)/LunarCycleLengthInDays;
+       //this should not be necessary, but you never know how screwed the FPU is...
+       if(p<0)p=0;
+       if(p>=LunarPhases_CycleLength)p=LunarPhases_CycleLength-1;
+       return LunarPhase(p);
+}
+
+
+}
diff --git a/caltools/src/solar.cpp b/caltools/src/solar.cpp
new file mode 100644 (file)
index 0000000..3d82d9e
--- /dev/null
@@ -0,0 +1,116 @@
+// Calculating Sun phases
+//
+// (c) Konrad Rosenbaum, 2017
+// protected under the GNU LGPL v3 or at your option any newer
+
+#include <solar.h>
+
+#include <math.h>
+
+
+void CalTools::GeoCoord::moveWest(double w)
+{
+       mwest+=w;
+       while(mwest>180)mwest-=360;
+       while(mwest<-180)mwest+=360;
+}
+
+void CalTools::GeoCoord::moveEast(double e){moveWest(-e);}
+
+void CalTools::GeoCoord::moveNorth(double n)
+{
+       mnorth+=n;
+       while(mnorth>180)mnorth-=360;
+       while(mnorth<-180)mnorth+=360;
+       if(mnorth>90){
+               moveWest(180);
+               mnorth=180-mnorth;
+       }
+       if(mnorth<-90){
+               moveWest(180);
+               mnorth=-180-mnorth;
+       }
+}
+
+void CalTools::GeoCoord::moveSouth(double s){moveNorth(-s);}
+
+static inline double deg2rad(double d){return d*M_PI/180.;}
+static inline double rad2deg(double r){return r*180./M_PI;}
+
+//argument of perihelion
+static inline double aperi(double n){return 102.9372 + 0.3179526*n/36525.;}
+
+CalTools::SolarInfo::SolarInfo(QDate date,GeoCoord geo)
+{
+       if(!date.isValid() || !geo.isValid())return;
+       mcoord=geo;
+       
+       // /////
+       // section: calculate midday UTC
+       
+       // day since 2000-1-1 plus slight correction
+       double n=date.toJulianDay() - 2451545.0 + 0.0008;
+       // mean solar noon
+       double J = n - mcoord.degWest()/360.;
+       // solar mean anomaly
+       double M = fmod((357.5291 + .98560028 * J), 360.);
+       // equation of the center
+       double C = 1.9148*sin(deg2rad(M)) + 0.02*sin(2*deg2rad(M)) + 0.0003*sin(3*deg2rad(M));
+       // ecliptic longitude
+       double lambda = fmod((M + C + 180. + aperi(n)), 360.);
+       // solar transit
+       double Jtrans=2451545.5 + J + 0.0053*sin(deg2rad(M)) - 0.0069*sin(deg2rad(2*lambda));
+       // convert to date and time
+       mdate=QDate::fromJulianDay(floor(Jtrans));
+       double t=Jtrans - floor(Jtrans);
+       mmidday=QTime::fromMSecsSinceStartOfDay(t*24*3600*1000);
+       
+       // /////
+       // section: calculate twilight diffs
+       auto calcdiff=[&](double angle)->double{
+               //declination of the sun
+               double sindelta=sin(deg2rad(lambda))*sin(deg2rad(23.44));
+               double delta=rad2deg(asin(sindelta));
+               //hour angle
+               double cosomega=(sin(deg2rad(angle))-sin(deg2rad(mcoord.degNorth()))*sindelta) / (cos(deg2rad(mcoord.degNorth()))*cos(deg2rad(delta)));
+               double omega=acos(cosomega);
+               //calculate difference
+               return omega/2./M_PI;
+       };
+       mbegin=calcdiff(0.83);
+       mcivil=calcdiff(6.0);
+       mnautic=calcdiff(12.0);
+       mastro=calcdiff(18.0);
+}
+
+QDateTime CalTools::SolarInfo::sunrise(TwilightType type)const
+{
+       switch(type){
+               case TwilightType::Begin:return gentime(-mbegin);
+               case TwilightType::Civil:return gentime(-mcivil);
+               case TwilightType::Nautical:return gentime(-mnautic);
+               case TwilightType::Astronomical:return gentime(-mastro);
+               //should not happen:
+               default:return QDateTime();
+       }
+}
+
+QDateTime CalTools::SolarInfo::sunset(TwilightType type)const
+{
+       switch(type){
+               case TwilightType::Begin:return gentime(mbegin);
+               case TwilightType::Civil:return gentime(mcivil);
+               case TwilightType::Nautical:return gentime(mnautic);
+               case TwilightType::Astronomical:return gentime(mastro);
+               //should not happen:
+               default:return QDateTime();
+       }
+}
+
+QDateTime CalTools::SolarInfo::gentime(double diff)const
+{
+       if(!mdate.isValid() || isnan(diff) || isinf(diff))
+               return QDateTime();
+
+       return highNoon().addSecs(diff*24*3600);
+}
diff --git a/caltools/src/src.pro b/caltools/src/src.pro
new file mode 100644 (file)
index 0000000..5ade2f5
--- /dev/null
@@ -0,0 +1,12 @@
+TEMPLATE = lib
+TARGET = caltools
+VERSION =
+QT -= gui
+
+DESTDIR = $$PWD/../../lib
+
+INCLUDEPATH += ../../include/caltools
+
+DEFINES += CALTOOLS_EXPORT=Q_DECL_EXPORT
+
+SOURCES += lunar.cpp easterdate.cpp solar.cpp
diff --git a/caltools/tests/easter/easter.pro b/caltools/tests/easter/easter.pro
new file mode 100644 (file)
index 0000000..0af49ba
--- /dev/null
@@ -0,0 +1,10 @@
+TEMPLATE=app
+TARGET=caltest
+
+QT+=testlib
+QT-=gui
+CONFIG += testcase
+
+include(../../../caltools.pri)
+
+SOURCES+=test2.cpp
diff --git a/caltools/tests/easter/test2.cpp b/caltools/tests/easter/test2.cpp
new file mode 100644 (file)
index 0000000..148946e
--- /dev/null
@@ -0,0 +1,96 @@
+#include "easterdate.h"
+#include <QDebug>
+#include <QTest>
+
+
+class ED{
+public:
+       ED(quint16 y,quint8 m1,quint8 d1,quint8 m2=0,quint8 d2=0){year=y;wmonth=m1;wday=d1;emonth=m2?m2:m1;eday=d2?d2:d1;}
+       ED(){year=0;wmonth=wday=emonth=eday=0;}
+       ED(const ED&)=default;
+       
+       inline bool isWest(QDate d)const{return d.year()==year && d.month()==wmonth && d.day()==wday;}
+       inline bool isEast(QDate d)const{return d.year()==year && d.month()==emonth && d.day()==eday;}
+       
+       inline QDate west()const{return QDate(year,wmonth,wday);}
+       inline QDate east()const{return QDate(year,emonth,eday);}
+       
+       quint16 year;
+       quint8 wmonth,wday,emonth,eday;
+};
+
+//https://en.wikipedia.org/wiki/List_of_dates_for_Easter
+QList<ED> initTV()
+{
+       QList<ED> r;
+       r<<ED(1997,3, 30, 4, 27)
+       <<ED(1998,4, 12, 4, 19)
+       <<ED(1999,4, 4, 4, 11)
+       <<ED(2000,4, 23, 4, 30)
+       <<ED(2001,4, 15)
+       <<ED(2002,3, 31, 5, 5)
+       <<ED(2003,4, 20, 4, 27)
+       <<ED(2004,4, 11)
+       <<ED(2005,3, 27, 5, 1)
+       <<ED(2006,4, 16, 4, 23)
+       <<ED(2007,4, 8)
+       <<ED(2008,3, 23, 4, 27)
+       <<ED(2009,4, 12, 4, 19)
+       <<ED(2010,4, 4)
+       <<ED(2011,4, 24)
+       <<ED(2012,4, 8, 4, 15)
+       <<ED(2013,3, 31, 5, 5)
+       <<ED(2014,4, 20)
+       <<ED(2015,4, 5, 4, 12)
+       <<ED(2016,3, 27, 5, 1)
+       <<ED(2017,4, 16)
+       <<ED(2018,4, 1, 4, 8)
+       <<ED(2019,4, 21, 4, 28)
+       <<ED(2020,4, 12, 4, 19)
+       <<ED(2021,4, 4, 5, 2)
+       <<ED(2022,4, 17, 4, 24)
+       <<ED(2023,4, 9, 4, 16)
+       <<ED(2024,3, 31, 5, 5)
+       <<ED(2025,4, 20)
+       <<ED(2026,4, 5, 4, 12)
+       <<ED(2027,3, 28, 5, 2)
+       <<ED(2028,4, 16)
+       <<ED(2029,4, 1, 4, 8)
+       <<ED(2030,4, 21, 4, 28)
+       <<ED(2031,4, 13)
+       <<ED(2032,3, 28, 5, 2)
+       <<ED(2033,4, 17, 4, 24)
+       <<ED(2034,4, 9)
+       <<ED(2035,3, 25, 4, 29)
+       <<ED(2036,4, 13, 4, 20)
+       <<ED(2037,4, 5);
+       return r;
+}
+
+class TClass:public QObject
+{
+       Q_OBJECT
+private slots:
+       void western_easter();
+       void eastern_easter();
+};
+
+#include "test2.moc"
+
+void TClass::western_easter()
+{
+       for(ED ed:initTV()){
+               QDate d=CalTools::easterSunday(ed.year);
+               QVERIFY(ed.isWest(d));
+       }
+}
+
+void TClass::eastern_easter()
+{
+       for(ED ed:initTV()){
+               QDate d=CalTools::orthodoxEasterSunday(ed.year);
+               QVERIFY(ed.isEast(d));
+       }
+}
+
+QTEST_MAIN(TClass)
\ No newline at end of file
diff --git a/caltools/tests/geo/geo.pro b/caltools/tests/geo/geo.pro
new file mode 100644 (file)
index 0000000..6ada449
--- /dev/null
@@ -0,0 +1,10 @@
+TEMPLATE = app
+TARGET = caltest
+
+QT+=testlib
+QT-=gui
+CONFIG += testcase
+
+include(../../../caltools.pri)
+
+SOURCES += geotest.cpp
diff --git a/caltools/tests/geo/geotest.cpp b/caltools/tests/geo/geotest.cpp
new file mode 100644 (file)
index 0000000..22a0419
--- /dev/null
@@ -0,0 +1,36 @@
+#include <solar.h>
+
+#include <QTest>
+
+
+using namespace CalTools;
+
+class GT:public QObject{
+       Q_OBJECT
+private slots:
+       void validity();
+       void moveLatitude();
+       void moveLongitude();
+};
+
+void GT::validity()
+{
+       GeoCoord c1;
+       QVERIFY(!c1.isValid());
+       GeoCoord c2(10,30);
+       QVERIFY(c2.isValid());
+       c2.moveWest(200);
+       QVERIFY(c2.isValid());
+       c2.moveNorth(100);
+       QVERIFY(c2.isValid());
+}
+
+void GT::moveLatitude()
+{
+       GeoCoord c1(10,30);
+       c1.moveNorth(10);
+       QCOMPARE(c1.degNorth(),20);
+}
+void GT::moveLongitude(){}
+
+QTEST_MAIN(GT)
\ No newline at end of file
diff --git a/caltools/tests/gregorian/gregorian.pro b/caltools/tests/gregorian/gregorian.pro
new file mode 100644 (file)
index 0000000..c653477
--- /dev/null
@@ -0,0 +1,10 @@
+TEMPLATE=app
+TARGET=caltest
+
+QT+=testlib
+QT-=gui
+CONFIG += testcase
+
+INCLUDEPATH+=../../../include/caltools
+
+SOURCES+=gregtest.cpp
diff --git a/caltools/tests/gregorian/gregtest.cpp b/caltools/tests/gregorian/gregtest.cpp
new file mode 100644 (file)
index 0000000..9d3ef84
--- /dev/null
@@ -0,0 +1,156 @@
+#include "gregorian.h"
+#include <QTest>
+
+using namespace CalTools;
+
+class TClass:public QObject
+{
+       Q_OBJECT
+private slots:
+       void leapyear();
+       void daysInMonth();
+       void weekDaysInMonth();
+       void firstDay();
+       void lastDay();
+       void dayAfter();
+       void dayBefore();
+};
+
+#include "gregtest.moc"
+
+void TClass::leapyear()
+{
+       QCOMPARE(isLeapYear(1904),true);
+       QCOMPARE(isLeapYear(2004),true);
+       QCOMPARE(isLeapYear(2000),true);
+       QCOMPARE(isLeapYear(1901),false);
+       QCOMPARE(isLeapYear(1902),false);
+       QCOMPARE(isLeapYear(1903),false);
+       QCOMPARE(isLeapYear(1900),false);
+       QCOMPARE(isLeapYear(1800),false);
+}
+
+void TClass::daysInMonth()
+{
+       QCOMPARE(numDaysInMonth(2000,1),(quint8)31);
+       QCOMPARE(numDaysInMonth(2000,2),(quint8)29);
+       QCOMPARE(numDaysInMonth(2001,2),(quint8)28);
+       QCOMPARE(numDaysInMonth(2000,3),(quint8)31);
+       QCOMPARE(numDaysInMonth(2000,4),(quint8)30);
+       QCOMPARE(numDaysInMonth(2000,5),(quint8)31);
+       QCOMPARE(numDaysInMonth(2000,6),(quint8)30);
+       QCOMPARE(numDaysInMonth(2000,7),(quint8)31);
+       QCOMPARE(numDaysInMonth(2000,8),(quint8)31);
+       QCOMPARE(numDaysInMonth(2000,9),(quint8)30);
+       QCOMPARE(numDaysInMonth(2000,10),(quint8)31);
+       QCOMPARE(numDaysInMonth(2000,11),(quint8)30);
+       QCOMPARE(numDaysInMonth(2000,12),(quint8)31);
+}
+
+void TClass::weekDaysInMonth()
+{
+       QCOMPARE(numWeekDaysInMonth(2017,5),4);
+       QCOMPARE(numWeekDaysInMonth(2017,5,0),4);
+       QCOMPARE(numWeekDaysInMonth(2017,5,7),4);
+       QCOMPARE(numWeekDaysInMonth(2017,5,1),5);
+       QCOMPARE(numWeekDaysInMonth(2017,5,2),5);
+       QCOMPARE(numWeekDaysInMonth(2017,5,3),5);
+       QCOMPARE(numWeekDaysInMonth(2017,5,4),4);
+       QCOMPARE(numWeekDaysInMonth(2017,5,5),4);
+       QCOMPARE(numWeekDaysInMonth(2017,5,6),4);
+}
+
+void TClass::firstDay()
+{
+       QCOMPARE(firstWeekDayInMonth(2017,5,1),QDate(2017,5,1));
+       QCOMPARE(firstWeekDayInMonth(2017,5,2),QDate(2017,5,2));
+       QCOMPARE(firstWeekDayInMonth(2017,5,3),QDate(2017,5,3));
+       QCOMPARE(firstWeekDayInMonth(2017,5,4),QDate(2017,5,4));
+       QCOMPARE(firstWeekDayInMonth(2017,5,5),QDate(2017,5,5));
+       QCOMPARE(firstWeekDayInMonth(2017,5,6),QDate(2017,5,6));
+       QCOMPARE(firstWeekDayInMonth(2017,5,7),QDate(2017,5,7));
+       QCOMPARE(firstWeekDayInMonth(2017,5,0),QDate(2017,5,7));
+
+       QCOMPARE(firstWeekDayInMonth(2017,6,4),QDate(2017,6,1));
+       QCOMPARE(firstWeekDayInMonth(2017,6,5),QDate(2017,6,2));
+       QCOMPARE(firstWeekDayInMonth(2017,6,6),QDate(2017,6,3));
+       QCOMPARE(firstWeekDayInMonth(2017,6,7),QDate(2017,6,4));
+       QCOMPARE(firstWeekDayInMonth(2017,6,0),QDate(2017,6,4));
+       QCOMPARE(firstWeekDayInMonth(2017,6,1),QDate(2017,6,5));
+       QCOMPARE(firstWeekDayInMonth(2017,6,2),QDate(2017,6,6));
+       QCOMPARE(firstWeekDayInMonth(2017,6,3),QDate(2017,6,7));
+
+       QCOMPARE(firstWeekDayInMonth(2017,6,33),QDate());
+}
+
+void TClass::lastDay()
+{
+       QCOMPARE(lastWeekDayInMonth(2017,5,1),QDate(2017,5,29));
+       QCOMPARE(lastWeekDayInMonth(2017,5,2),QDate(2017,5,30));
+       QCOMPARE(lastWeekDayInMonth(2017,5,3),QDate(2017,5,31));//
+       QCOMPARE(lastWeekDayInMonth(2017,5,4),QDate(2017,5,25));
+       QCOMPARE(lastWeekDayInMonth(2017,5,5),QDate(2017,5,26));
+       QCOMPARE(lastWeekDayInMonth(2017,5,6),QDate(2017,5,27));
+       QCOMPARE(lastWeekDayInMonth(2017,5,7),QDate(2017,5,28));
+       QCOMPARE(lastWeekDayInMonth(2017,5,0),QDate(2017,5,28));
+
+       QCOMPARE(lastWeekDayInMonth(2017,5,10),QDate());
+}
+
+void TClass::dayAfter()
+{
+       const QDate d(2017,5,17);
+       QCOMPARE(findDayAfter(d),QDate(2017,5,21));
+       QCOMPARE(findDayAfter(d,0),QDate(2017,5,21));
+       QCOMPARE(findDayAfter(d,7),QDate(2017,5,21));
+       QCOMPARE(findDayAfter(d,0,false),QDate(2017,5,21));
+       QCOMPARE(findDayAfter(d,0,false),QDate(2017,5,21));
+       
+       QCOMPARE(findDayAfter(d,1,false),QDate(2017,5,22));
+       QCOMPARE(findDayAfter(d,2,false),QDate(2017,5,23));
+       QCOMPARE(findDayAfter(d,3,false),QDate(2017,5,24));
+       QCOMPARE(findDayAfter(d,4,false),QDate(2017,5,18));
+       QCOMPARE(findDayAfter(d,5,false),QDate(2017,5,19));
+       QCOMPARE(findDayAfter(d,6,false),QDate(2017,5,20));
+       QCOMPARE(findDayAfter(d,7,false),QDate(2017,5,21));
+
+       QCOMPARE(findDayAfter(d,1,true),QDate(2017,5,22));
+       QCOMPARE(findDayAfter(d,2,true),QDate(2017,5,23));
+       QCOMPARE(findDayAfter(d,3,true),QDate(2017,5,17));
+       QCOMPARE(findDayAfter(d,4,true),QDate(2017,5,18));
+       QCOMPARE(findDayAfter(d,5,true),QDate(2017,5,19));
+       QCOMPARE(findDayAfter(d,6,true),QDate(2017,5,20));
+       QCOMPARE(findDayAfter(d,7,true),QDate(2017,5,21));
+
+       QCOMPARE(findDayAfter(d,9,false),QDate());
+}
+
+void TClass::dayBefore()
+{
+       const QDate d(2017,5,17);
+       QCOMPARE(findDayBefore(d),QDate(2017,5,14));
+       QCOMPARE(findDayBefore(d,0),QDate(2017,5,14));
+       QCOMPARE(findDayBefore(d,7),QDate(2017,5,14));
+       QCOMPARE(findDayBefore(d,0,false),QDate(2017,5,14));
+       QCOMPARE(findDayBefore(d,0,false),QDate(2017,5,14));
+       
+       QCOMPARE(findDayBefore(d,1,false),QDate(2017,5,15));
+       QCOMPARE(findDayBefore(d,2,false),QDate(2017,5,16));
+       QCOMPARE(findDayBefore(d,3,false),QDate(2017,5,10));
+       QCOMPARE(findDayBefore(d,4,false),QDate(2017,5,11));
+       QCOMPARE(findDayBefore(d,5,false),QDate(2017,5,12));
+       QCOMPARE(findDayBefore(d,6,false),QDate(2017,5,13));
+       QCOMPARE(findDayBefore(d,7,false),QDate(2017,5,14));
+
+       QCOMPARE(findDayBefore(d,1,true),QDate(2017,5,15));
+       QCOMPARE(findDayBefore(d,2,true),QDate(2017,5,16));
+       QCOMPARE(findDayBefore(d,3,true),QDate(2017,5,17));
+       QCOMPARE(findDayBefore(d,4,true),QDate(2017,5,11));
+       QCOMPARE(findDayBefore(d,5,true),QDate(2017,5,12));
+       QCOMPARE(findDayBefore(d,6,true),QDate(2017,5,13));
+       QCOMPARE(findDayBefore(d,7,true),QDate(2017,5,14));
+
+       QCOMPARE(findDayBefore(d,9,false),QDate());
+}
+
+QTEST_MAIN(TClass)
\ No newline at end of file
diff --git a/caltools/tests/lunar/lunar.cpp b/caltools/tests/lunar/lunar.cpp
new file mode 100644 (file)
index 0000000..4b6f52f
--- /dev/null
@@ -0,0 +1,28 @@
+#include "lunar.h"
+#include <QTest>
+
+using namespace CalTools;
+
+class TClass:public QObject
+{
+       Q_OBJECT
+private slots:
+       void lunarPhases();
+};
+
+#include "lunar.moc"
+
+void TClass::lunarPhases()
+{
+       QCOMPARE(lunarPhaseForDate(QDate(2017,4,26)),LunarPhase::NewMoon);
+       QCOMPARE(lunarPhaseForDate(QDate(2017,5,1)),LunarPhase::Waxing25);
+       QCOMPARE(lunarPhaseForDate(QDate(2017,5,3)),LunarPhase::Waxing50);
+       QCOMPARE(lunarPhaseForDate(QDate(2017,5,6)),LunarPhase::Waxing75);
+       QCOMPARE(lunarPhaseForDate(QDate(2017,5,10)),LunarPhase::FullMoon);
+       QCOMPARE(lunarPhaseForDate(QDate(2017,5,16)),LunarPhase::Waning75);
+       QCOMPARE(lunarPhaseForDate(QDate(2017,5,19)),LunarPhase::Waning50);
+       QCOMPARE(lunarPhaseForDate(QDate(2017,5,21)),LunarPhase::Waning25);
+       QCOMPARE(lunarPhaseForDate(QDate(2017,5,25)),LunarPhase::NewMoon);
+}
+
+QTEST_MAIN(TClass)
\ No newline at end of file
diff --git a/caltools/tests/lunar/lunar.pro b/caltools/tests/lunar/lunar.pro
new file mode 100644 (file)
index 0000000..4f44a4f
--- /dev/null
@@ -0,0 +1,10 @@
+TEMPLATE=app
+TARGET=caltest
+
+QT+=testlib
+QT-=gui
+CONFIG += testcase
+
+include(../../../caltools.pri)
+
+SOURCES+=lunar.cpp
diff --git a/caltools/tests/solar/solar.pro b/caltools/tests/solar/solar.pro
new file mode 100644 (file)
index 0000000..bd69f01
--- /dev/null
@@ -0,0 +1,8 @@
+TEMPLATE = app
+TARGET = caltest
+
+QT-=gui
+
+include(../../../caltools.pri)
+
+SOURCES += solartest.cpp
diff --git a/caltools/tests/solar/solartest.cpp b/caltools/tests/solar/solartest.cpp
new file mode 100644 (file)
index 0000000..77f394b
--- /dev/null
@@ -0,0 +1,3 @@
+#include <solar.h>
+
+int main(){}
\ No newline at end of file
diff --git a/include/caltools/caltools.h b/include/caltools/caltools.h
new file mode 100644 (file)
index 0000000..42b7bb7
--- /dev/null
@@ -0,0 +1,4 @@
+#include "easterdate.h"
+#include "gregorian.h"
+#include "lunar.h"
+#include "solar.h"
diff --git a/include/caltools/easterdate.h b/include/caltools/easterdate.h
new file mode 100644 (file)
index 0000000..43113bd
--- /dev/null
@@ -0,0 +1,43 @@
+// Calculating Easter Dates
+//
+// (c) Konrad Rosenbaum, 2017
+// protected under the GNU LGPL v3 or at your option any newer
+
+#ifndef CALTOOLS_EASTER_H
+#define CALTOOLS_EASTER_H
+
+#include <QDate>
+
+#if 0
+#include <QDebug>
+#define DD(x) qDebug()<<""#x "="<<x;
+#else
+#define DD(x)
+#endif
+
+#ifndef CALTOOLS_EXPORT
+#define CALTOOLS_EXPORT Q_DECL_IMPORT
+#endif
+
+
+// formulas were found at http://www.dateofeaster.com/
+// some more explanations (in German): https://de.wikipedia.org/wiki/Osterzyklus#Die_Mondgleichung
+//   and: https://de.wikipedia.org/wiki/Gau%C3%9Fsche_Osterformel
+
+namespace CalTools {
+
+///calculates the date of easter for a specific year
+QDate CALTOOLS_EXPORT easterSunday(quint16 year);
+
+///calculates the eastern orthodox date for Easter sunday
+QDate CALTOOLS_EXPORT orthodoxEasterSunday(quint16 year);
+
+//TODO: date of jewish pessach
+
+
+//end of namespace
+}
+
+#undef DD
+
+#endif
diff --git a/include/caltools/gregorian.h b/include/caltools/gregorian.h
new file mode 100644 (file)
index 0000000..cdfe5a1
--- /dev/null
@@ -0,0 +1,129 @@
+// Calculating various aspects of the Gregorian calendar
+//
+// (c) Konrad Rosenbaum, 2017
+// protected under the GNU LGPL v3 or at your option any newer
+
+#ifndef CALTOOLS_GREGORIAN_H
+#define CALTOOLS_GREGORIAN_H
+
+#include <QDate>
+
+namespace CalTools{
+
+// https://en.wikipedia.org/wiki/Leap_year
+/// checks for leap years (works only within the Gregorian Calendar!)
+static inline
+bool isLeapYear(quint16 year)
+{
+       if(year%4 == 0){
+               if(year%100 == 0){
+                       if(year%400 == 0)return true;
+                       return false;
+               }
+               return true;
+       }
+       return false;
+}
+
+///returns how many days there are in a specific month
+static inline
+quint8 numDaysInMonth(quint16 year,quint8 month)
+{
+       switch(month){
+               case 1:case 3:case 5:case 7:
+               case 8:case 10:case 12:
+                       return 31;
+               case 4:case 6:case 9:case 11:
+                       return 30;
+               case 2:
+                       return 28+(isLeapYear(year)?1:0);
+               default:
+                       return 0;
+       }
+}
+
+///finds the first instance of a week day in a month
+/// \param year the Year in which to search
+/// \param month the Month in which to search
+/// \param dayOfWeek the week day to look for: 0/7 is sunday (\see QDate)
+static inline
+QDate firstWeekDayInMonth(quint16 year,quint8 month,quint8 dayOfWeek=0)
+{
+       if(dayOfWeek==0)dayOfWeek=7;
+       if(dayOfWeek>7)return QDate();
+       //check first day
+       QDate d(year,month,1);
+       int dw=d.dayOfWeek();
+       if(dw==dayOfWeek)return d;
+       //move on
+       if(dayOfWeek<dw)dayOfWeek+=7;
+       return d.addDays(dayOfWeek-dw);
+}
+
+///finds how many instances of a specific weekday there are in a month
+/// \param year the Year in which to search
+/// \param month the Month in which to search
+/// \param dayOfWeek the week day to look for: 0/7 is sunday (\see QDate)
+static inline
+int numWeekDaysInMonth(quint16 year,quint8 month,quint8 dayOfWeek=0)
+{
+       if(dayOfWeek==0)dayOfWeek=7;
+       if(dayOfWeek>7)return 0;
+       //get first day of that type, then calculate
+       int d=firstWeekDayInMonth(year,month,dayOfWeek).day();
+       return (numDaysInMonth(year,month)-d)/7 + 1;
+}
+
+///finds the last instance of a week day in a month
+/// \param year the Year in which to search
+/// \param month the Month in which to search
+/// \param dayOfWeek the week day to look for: 0/7 is sunday (\see QDate)
+static inline
+QDate lastWeekDayInMonth(quint16 year,quint8 month,quint8 dayOfWeek=0)
+{
+       if(dayOfWeek==0)dayOfWeek=7;
+       if(dayOfWeek>7)return QDate();
+       //check last day
+       QDate d(year,month,numDaysInMonth(year,month));
+       int dw=d.dayOfWeek();
+       if(dw==dayOfWeek)return d;
+       if(dw<dayOfWeek)dw+=7;
+       return d.addDays(dayOfWeek-dw);
+}
+
+///find the first instance of a specific week day after a given date
+/// \param date the date from which to search
+/// \param dayOfWeek the week day to look for (default is sunday)
+/// \param inclusive if true date is returned if it is the week day in question, otherwise a date 7 days later
+static inline
+QDate findDayAfter(const QDate &date,quint8 dayOfWeek=0,bool inclusive=false)
+{
+       if(dayOfWeek==0)dayOfWeek=7;
+       if(dayOfWeek>7)return QDate();
+       //same one?
+       int dw=date.dayOfWeek();
+       if(inclusive && dw==dayOfWeek)return date;
+       if(dayOfWeek<=dw)dayOfWeek+=7;
+       return date.addDays(dayOfWeek-dw);
+}
+
+///find the first instance of a specific week day before a given date
+/// \param date the date from which to search
+/// \param dayOfWeek the week day to look for (default is sunday)
+/// \param inclusive if true date is returned if it is the week day in question, otherwise a date 7 days earlier
+static inline
+QDate findDayBefore(QDate date,quint8 dayOfWeek=0,bool inclusive=false)
+{
+       if(dayOfWeek==0)dayOfWeek=7;
+       if(dayOfWeek>7)return QDate();
+       //same one?
+       int dw=date.dayOfWeek();
+       if(inclusive && dw==dayOfWeek)return date;
+       if(dayOfWeek>=dw)dw+=7;
+       return date.addDays(dayOfWeek-dw);
+}
+
+//end of namespace
+}
+
+#endif
diff --git a/include/caltools/lunar.h b/include/caltools/lunar.h
new file mode 100644 (file)
index 0000000..f890e29
--- /dev/null
@@ -0,0 +1,60 @@
+// Calculating Easter Dates
+//
+// (c) Konrad Rosenbaum, 2017
+// protected under the GNU LGPL v3 or at your option any newer
+
+#ifndef CALTOOLS_LUNAR_H
+#define CALTOOLS_LUNAR_H
+
+#include <QDate>
+
+namespace CalTools{
+
+///Phases of the Moon (note: this only applies to earth's moon, not to Tatooine, Europa, Io or any other moon)
+enum class LunarPhase {
+       ///New Moon, not visible with the naked eye
+       NewMoon=0,
+       ///getting bigger, it's a crescent
+       Waxing25=1,
+       ///getting bigger, it's a half moon
+       Waxing50=2,
+       ///getting bigger, it's almost full
+       Waxing75=3,
+       ///getting bigger, it's a crescent
+       WaxingCrescent=1,
+       ///getting bigger, it's a half moon
+       WaxingHalf=2,
+       ///getting bigger, it's almost full
+       WaxingGibbous=3,
+       ///getting bigger, it's a half moon, first quarter of the full cycle
+       FirstQuarter=2,
+       ///full moon
+       FullMoon=4,
+       ///getting smaller, it's almost full
+       Waning75=5,
+       ///getting smaller, it's a half moon
+       Waning50=6,
+       ///getting smaller, it's a crescent
+       Waning25=7,
+       ///getting smaller, it's almost full
+       WaningGibbous=5,
+       ///getting smaller, it's a half moon
+       WaningHalf=6,
+       ///getting smaller, it's a crescent
+       WaningCrescent=7,
+       ///getting smaller, it's a half moon, third quarter of the full cycle
+       ThirdQuarter=6,
+};
+
+#ifndef CALTOOLS_EXPORT
+#define CALTOOLS_EXPORT Q_DECL_IMPORT
+#endif
+
+///returns the phase of the moon for a specific date
+/// note: this function is quite 
+LunarPhase lunarPhaseForDate(QDate d);
+
+//end of namespace
+}
+
+#endif
diff --git a/include/caltools/solar.h b/include/caltools/solar.h
new file mode 100644 (file)
index 0000000..a2df863
--- /dev/null
@@ -0,0 +1,126 @@
+// Calculating Sun phases
+//
+// (c) Konrad Rosenbaum, 2017
+// protected under the GNU LGPL v3 or at your option any newer
+
+#ifndef CALTOOLS_SOLAR_H
+#define CALTOOLS_SOLAR_H
+
+#include <QDateTime>
+
+namespace CalTools {
+
+//TODO: calculate sunrise/sundown
+// https://en.wikipedia.org/wiki/Sunrise_equation
+
+#ifndef CALTOOLS_EXPORT
+#define CALTOOLS_EXPORT Q_DECL_IMPORT
+#endif
+
+///Represents geographical coordinates.
+class CALTOOLS_EXPORT GeoCoord
+{
+       double mwest=1000,mnorth=0;
+public:
+       ///Instantiates an invalid coordinate instance.
+       GeoCoord(){}
+       ///Instantiates a coordinate instance.
+       ///\param north - the latitude in degrees north, negative values are south (-90 .. 0 .. 90)
+       ///\param west - the longitude in degrees west, negative values are east (-180 .. 0 .. 180)
+       GeoCoord(double north,double west):mwest(west),mnorth(north){/*for the corrections*/ moveWest(0);moveNorth(0);}
+       ///Copies the instance.
+       GeoCoord(const GeoCoord&)=default;
+       ///Moves the instance.
+       GeoCoord(GeoCoord&&)=default;
+       
+       ///Copies the instance.
+       GeoCoord& operator=(const GeoCoord&)=default;
+       ///Moves the instance.
+       GeoCoord& operator=(GeoCoord&&)=default;
+       
+       ///returns true if the coordinates are valid
+       bool isValid()const{return mwest<=180 && mwest>=-180 && mnorth<=90 && mnorth>=-90;}
+       ///returns the longitude in degrees west (negative is east)
+       double degWest()const{return mwest;}
+       ///returns the longitude in degrees east (negative is west)
+       double degEast()const{return -mwest;}
+       ///returns the latitude in degrees north (negative is south)
+       double degNorth()const{return mnorth;}
+       ///returns the latitude in degrees south (negative is north)
+       double degSouth()const{return -mnorth;}
+       
+       ///moves the coordinates further west
+       void moveWest(double);
+       ///moves the coordinates further east
+       void moveEast(double);
+       ///moves the coordinates further north (can wrap around beyond the north pole)
+       void moveNorth(double);
+       ///moves the coordinates further south (can wrap around beyond the south pole)
+       void moveSouth(double);
+};
+
+///Calculator for sunset and sunrise
+class CALTOOLS_EXPORT SolarInfo
+{
+public:
+       ///Instantiates an invalid instance that will return invalid times for all values.
+       SolarInfo(){}
+       ///Instantiates an instance for the given date and coordinates on earth.
+       SolarInfo(QDate date,GeoCoord geo);
+       
+       ///copies the instance
+       SolarInfo(const SolarInfo&)=default;
+       ///moves the instance
+       SolarInfo(SolarInfo&&)=default;
+       
+       ///copies the instance
+       SolarInfo& operator=(const SolarInfo&)=default;
+       ///moves the instance
+       SolarInfo& operator=(SolarInfo&&)=default;
+       
+       ///returns true if the solar calculator has valid date and coordinates
+       bool isValid()const{return mdate.isValid();}
+       
+       ///What exact part of twilight we are calculating.
+       enum class TwilightType {
+               ///Begin of sunset or sunrise - when the sun touches the horizon.
+               Begin,
+               ///End of civil twilight - the sun is 6 degrees below the horizon.
+               Civil,
+               ///End of nautic twilight - the sun is 12 degrees below the horizon.
+               Nautical,
+               ///End of astronomical twilight - the sun is 18 degrees below the horizon.
+               Astronomical
+       };
+       
+       ///Calculate sunrise.
+       ///\param type the phase of sunrise that will be calculated
+       ///\returns the time of the sunrise phase in UTC
+       QDateTime sunrise(TwilightType type=TwilightType::Begin)const;
+
+       ///Calculate sunset.
+       ///\param type the phase of sunset that will be calculated
+       ///\returns the time of the sunset phase in UTC
+       QDateTime sunset(TwilightType type=TwilightType::Begin)const;
+       
+       ///\returns The time of highest sun (zenith).
+       QDateTime highNoon()const{return QDateTime(mdate,mmidday,Qt::UTC);}
+       
+private:
+       ///the date for which we calculate times
+       QDate mdate;
+       ///the time at which the sun is at the zenith in UTC
+       QTime mmidday;
+       ///time differences in fractions of a day
+       double mbegin=0,mcivil=0,mnautic=0,mastro=0;
+       ///geographical coordinates
+       GeoCoord mcoord;
+       
+       ///calculate a time relative to the calculated zenith
+       QDateTime gentime(double diff)const;
+};
+
+//end of namespace
+}
+
+#endif
index 9eb0d1b..946a878 100644 (file)
@@ -1,3 +1,3 @@
 TEMPLATE = subdirs
 
-SUBDIRS = zip elam aurora
+SUBDIRS = zip elam aurora caltools