Skip to content

Commit

Permalink
add error handling and add optional interval specification
Browse files Browse the repository at this point in the history
  • Loading branch information
jkittner committed Oct 15, 2023
1 parent 47f7273 commit aa27a15
Show file tree
Hide file tree
Showing 5 changed files with 157 additions and 29 deletions.
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ COPY ./pg_solpos.c .
COPY ./solpos* .

RUN : \
&& cc -fPIC -Werror -c solpos.c \
&& cc -fPIC -Werror -c pg_solpos.c -lm -I /usr/include/postgresql/${VERSION}/server \
&& cc -fPIC -Werror -Wall -c solpos.c \
&& cc -fPIC -Werror -Wall -c pg_solpos.c -lm -I /usr/include/postgresql/${VERSION}/server \
&& cc -shared -o pg_solpos.so pg_solpos.o solpos.o \
&& find . -name "*solpos*" | grep -v pg_solpos.so | xargs rm
28 changes: 25 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,36 @@ Calculate the true solar time of a date and a location.
| `lat` | DOUBLE PRECISION | the latitude in decimal degrees of the position |
| `lon` | DOUBLE PRECISION | the longitude in decimal degrees of the position |

**Optional arguments**

| **Name** | **Type** | **Description** |
| --------------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `data_interval` | INT DEFAULT 0 | Interval of a measurement period in seconds. Forces solpos to use the time and date from the interval midpoint. The INPUT time is assumed to be the **end** of the measurement interval. |

**Sample usage**

Get the solar time for a date at a selected position:

```sql
SELECT solar_time('2023-10-06 15:30+00:00', 51.481, 7.217)
```

```console
solar_time
--------------------------
2023-10-06 16:10:43.7+00
solar_time
-----------------------
2023-10-06 16:10:43.7
```

Get the solar time for a date at a selected position. Each timestamp is a one-hourly interval labelled at the **end** of the interval:

```sql
SELECT solar_time('2023-10-06 15:30+00:00', 51.481, 7.217, 3600)
```

```console
solar_time
-------------------------
2023-10-06 16:10:43.425
```

### References
Expand Down
70 changes: 62 additions & 8 deletions pg_solpos.c
Original file line number Diff line number Diff line change
@@ -1,45 +1,99 @@
#include "postgres.h"
#include "fmgr.h"
#include "solpos00.h"
#include "utils/datetime.h"
#include "utils/timestamp.h"

PG_MODULE_MAGIC;

/* handle an error within the solpos part and provide a useful error message to the
* user.
*/
void
handle_error(const long code, struct posdata *pdat, TimestampTz ts)
{
const char *ts_str = timestamptz_to_str(ts);

if (code & (1L << S_YEAR_ERROR)) {
elog(ERROR, "%s: Invalid year: %d (allowed range: [1950-2050])", ts_str,
pdat->year);
}
else if (code & (1L << S_MONTH_ERROR)) {
elog(ERROR, "%s: Invalid month: %d", ts_str, pdat->month);
}
else if (code & (1L << S_DAY_ERROR)) {
elog(ERROR, "%s: Invalid day-of-month: %d", ts_str, pdat->day);
}
else if (code & (1L << S_HOUR_ERROR)) {
elog(ERROR, "%s: Invalid hour: %d", ts_str, pdat->hour);
}
else if (code & (1L << S_MINUTE_ERROR)) {
elog(ERROR, "%s: Invalid minute: %d", ts_str, pdat->minute);
}
else if (code & (1L << S_SECOND_ERROR))
elog(ERROR, "%s: Invalid second: %d", ts_str, pdat->second);
else if (code & (1L << S_TZONE_ERROR)) {
elog(ERROR, "%s: Invalid time zone: %f", ts_str, pdat->timezone);
}
else if (code & (1L << S_INTRVL_ERROR)) {
elog(ERROR, "Invalid data_interval: %d (allowed range: [0 - 28800])",
pdat->interval);
}
else if (code & (1L << S_LAT_ERROR)) {
elog(ERROR, "Invalid latitude: %f", pdat->latitude);
}
else if (code & (1L << S_LON_ERROR)) {
elog(ERROR, "Invalid longitude: %f", pdat->longitude);
}
else {
elog(ERROR,

"An unknown error occurred while calling: solar_time(%s, %f, %f, %d)",
ts_str, pdat->latitude, pdat->longitude, pdat->interval);
}
}

PG_FUNCTION_INFO_V1(solar_time);

Datum
solar_time(PG_FUNCTION_ARGS)
{
pg_time_t orig_time_t = timestamptz_to_time_t(PG_GETARG_TIMESTAMPTZ(0));
TimestampTz ts = PG_GETARG_TIMESTAMPTZ(0);
/* this takes the timestamp into account making it a UNIX timestamp in UTC */
pg_time_t orig_time_t = timestamptz_to_time_t(ts);
float8 latitude = PG_GETARG_FLOAT8(1);
float8 longitude = PG_GETARG_FLOAT8(2);
int64 data_interval = PG_GETARG_INT64(3);
/* create a pg_tm struct so we can access the date and time components */
struct pg_tm *tm_result;
tm_result = pg_localtime(&orig_time_t, log_timezone);
tm_result = pg_gmtime(&orig_time_t);

/* setup the solpos calculations */
struct posdata pd, *pdat;
pdat = &pd;
S_init(pdat);

pdat->function &= (S_TST & ~S_DOY);
/* see pgtime.h. Year is relative to 1900 and month has its origin at 0 */
pdat->year = tm_result->tm_year + 1900;
pdat->month = tm_result->tm_mon + 1;
pdat->day = tm_result->tm_mday;
pdat->hour = tm_result->tm_hour;
pdat->minute = tm_result->tm_min;
pdat->second = tm_result->tm_sec;
pdat->interval = data_interval;
/* this should always be 0 since we use pg_gmtime*/
pdat->timezone = tm_result->tm_gmtoff;
pdat->longitude = longitude;
pdat->latitude = latitude;

long retval = S_solpos(pdat);
const long retval = S_solpos(pdat);

if (retval != 0) {
elog(ERROR, "internal error in the solpos function");
handle_error(retval, pdat, ts);
}

/* add the offset to the date and calculate the result */
int64 offset = (int64)(pdat->tstfix * 60000);
TimestampTz new_time = time_t_to_timestamptz(orig_time_t);
new_time = TimestampTzPlusMilliseconds(new_time, offset);
PG_RETURN_TIMESTAMPTZ(new_time);
/* return this as a tz naive timestamp since it's solar time */
PG_RETURN_TIMESTAMP(new_time);
}
11 changes: 8 additions & 3 deletions pg_solpos.sql
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
CREATE FUNCTION solar_time(ts timestamptz, lat double precision, lon double precision) RETURNS timestamptz
AS '/usr/local/lib/funcs/pg_solpos.so', 'solar_time'
LANGUAGE C STRICT;
CREATE FUNCTION solar_time(
ts TIMESTAMPTZ,
lat DOUBLE PRECISION,
lon DOUBLE PRECISION,
data_interval INT DEFAULT 0
) RETURNS TIMESTAMP WITHOUT TIME ZONE
AS '/usr/local/lib/funcs/pg_solpos.so', 'solar_time'
LANGUAGE C STRICT;
73 changes: 60 additions & 13 deletions tests/solpos_test.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from datetime import datetime
from datetime import timezone

import psycopg
import psycopg.errors
import pytest


Expand Down Expand Up @@ -34,7 +33,7 @@ def _test_postgresql(dsn):

@pytest.fixture(scope='session')
def db(postgresql, docker_services):
with psycopg.connect(postgresql) as conn:
with psycopg.connect(postgresql, autocommit=True) as conn:
yield conn


Expand All @@ -47,18 +46,18 @@ def initialize_sql_function(db: psycopg.Connection) -> None:

def test_solar_time_date_tz_naive(db):
with db.cursor() as cur:
cur.execute("SELECT solar_time('2023-10-06 15:30'::TIMESTAMP, 51.481, 7.217)") # noqa: E50
cur.execute("SELECT solar_time('2023-10-06 15:30'::TIMESTAMP, 51.481, 7.217)") # noqa: E501
d, = cur.fetchone()

assert d == datetime(2023, 10, 6, 16, 10, 43, 700000, tzinfo=timezone.utc)
assert d == datetime(2023, 10, 6, 16, 10, 43, 700000)


def test_solar_time_date_tz_aware(db):
with db.cursor() as cur:
cur.execute("SELECT solar_time('2023-10-06 15:30+02:00'::TIMESTAMPTZ, 51.481, 7.217)") # noqa: E501
d, = cur.fetchone()

assert d == datetime(2023, 10, 6, 14, 10, 42, 363000, tzinfo=timezone.utc)
assert d == datetime(2023, 10, 6, 14, 10, 42, 363000)


def test_solar_time_date(db):
Expand All @@ -68,28 +67,76 @@ def test_solar_time_date(db):
)
d, = cur.fetchone()

assert d == datetime(2023, 10, 6, 0, 40, 32, 94000, tzinfo=timezone.utc)
assert d == datetime(2023, 10, 6, 0, 40, 32, 94000)


def test_solar_time_lat_numeric(db):
with db.cursor() as cur:
cur.execute("SELECT solar_time('2023-10-06 15:30'::TIMESTAMP, 51.481::NUMERIC, 7.217)") # noqa: E50
cur.execute("SELECT solar_time('2023-10-06 15:30'::TIMESTAMP, 51.481::NUMERIC, 7.217)") # noqa: E501
d, = cur.fetchone()

assert d == datetime(2023, 10, 6, 16, 10, 43, 700000, tzinfo=timezone.utc)
assert d == datetime(2023, 10, 6, 16, 10, 43, 700000)


def test_solar_time_lon_int(db):
with db.cursor() as cur:
cur.execute("SELECT solar_time('2023-10-06 15:30'::TIMESTAMP, 51.481, 7.217::INT)") # noqa: E50
cur.execute("SELECT solar_time('2023-10-06 15:30'::TIMESTAMP, 51.481, 7.217::INT)") # noqa: E501
d, = cur.fetchone()

assert d == datetime(2023, 10, 6, 16, 9, 51, 621000, tzinfo=timezone.utc)
assert d == datetime(2023, 10, 6, 16, 9, 51, 621000)


def test_solar_time_lat_int(db):
with db.cursor() as cur:
cur.execute("SELECT solar_time('2023-10-06 15:30'::TIMESTAMP, 51.481::INT, 7.217)") # noqa: E50
cur.execute("SELECT solar_time('2023-10-06 15:30'::TIMESTAMP, 51.481::INT, 7.217)") # noqa: E501
d, = cur.fetchone()

assert d == datetime(2023, 10, 6, 16, 10, 43, 700000, tzinfo=timezone.utc)
assert d == datetime(2023, 10, 6, 16, 10, 43, 700000)


def test_solar_time_different_timezone(db):
# Australia has strange half hour TZs
with db.cursor() as cur:
cur.execute("SELECT solar_time('2023-10-06 15:30:00 ACST', -34.885, 138.579)") # noqa: E501
d, = cur.fetchone()

assert d == datetime(2023, 10, 6, 15, 26, 3, 484000)


@pytest.mark.parametrize('lat', (-91, 91))
def test_solar_time_invalid_latitude(db, lat):
with db.cursor() as cur:
with pytest.raises(psycopg.errors.InternalError) as exc:
cur.execute(f"SELECT solar_time('2023-10-06 15:30'::TIMESTAMP, {lat}, 7.217)") # noqa: E501

msg, = exc.value.args
assert msg == f'Invalid latitude: {lat}.000000'


@pytest.mark.parametrize('lon', (-181, 181))
def test_solar_time_invalid_longitude(db, lon):
with db.cursor() as cur:
with pytest.raises(psycopg.errors.InternalError) as exc:
cur.execute(f"SELECT solar_time('2023-10-06 15:30'::TIMESTAMP, 51.481, {lon})") # noqa: E501

msg, = exc.value.args
assert msg == f'Invalid longitude: {lon}.000000'


@pytest.mark.parametrize('interval', (-1, 28801))
def test_solar_time_with_invalid_interval_provided(db, interval):
with db.cursor() as cur:
with pytest.raises(psycopg.errors.InternalError) as exc:
cur.execute(f"SELECT solar_time('2023-10-06 15:30', 51.481, 7.217, {interval})") # noqa: E501

msg, = exc.value.args
assert msg == f'Invalid data_interval: {interval} (allowed range: [0 - 28800])' # noqa: E501


def test_solar_time_with_valid_interval_provided(db):
with db.cursor() as cur:
cur.execute("SELECT solar_time('2023-10-06 15:30', 51.481, 7.217, 3600)") # noqa: E50
d, = cur.fetchone()
print(d)

assert d == datetime(2023, 10, 6, 16, 10, 43, 425000)

0 comments on commit aa27a15

Please sign in to comment.