Sun position
Sun position algorithm
skyio.c
1 /*==============================================================================
2  * skyio.c - output and formatting routines and a read routine
3  *
4  * Author: David Hoadley
5  *
6  * Description: (see skyio.h)
7  *
8  * Copyright (c) 2020, David Hoadley <vcrumble@westnet.com.au>
9  * All rights reserved.
10  *
11  * Redistribution and use in source and binary forms, with or without
12  * modification, are permitted provided that the following conditions are met:
13  *
14  * * Redistributions of source code must retain the above copyright notice, this
15  * list of conditions and the following disclaimer.
16  * * Redistributions in binary form must reproduce the above copyright notice,
17  * this list of conditions and the following disclaimer in the documentation
18  * and/or other materials provided with the distribution.
19  *
20  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23  * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
24  * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
25  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
26  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
27  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
28  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
29  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30  * POSSIBILITY OF SUCH DAMAGE.
31  *
32  *==============================================================================
33  */
34 /*------------------------------------------------------------------------------
35  * Notes:
36  * Character set: UTF-8. (Non-ASCII characters appear in this file)
37  *----------------------------------------------------------------------------*/
38 
39 /* ANSI includes etc. */
40 #include <ctype.h>
41 #include <math.h>
42 #include <stdlib.h>
43 #include <stdio.h>
44 #include <string.h> /* for memcpy() */
45 
46 /* Local and project includes */
47 #include "skyio.h"
48 
49 #include "general.h"
50 #include "sky.h"
51 
52 /*
53  * Local #defines and typedefs
54  */
55 DEFINE_THIS_FILE; /* For use by REQUIRE() - assertions.*/
56 
57 /*
58  * Prototypes for local functions (not called from other modules)
59  */
60 
61 /*
62  * Global variables accessible by other modules
63  */
64 
65 
66 /*
67  * Local variables (not accessed by other modules)
68  */
69 LOCAL const char digits[] = "0123456789";
70 LOCAL const char cDegree[] = "°";
71 LOCAL const char cMinute[] = "′";
72 LOCAL const char cSecond[] = "″";
73 
74 LOCAL const char decimalChar = '.'; // use ',' if you prefer.
75 LOCAL const char fieldSeparator = ',';
76 
77 /*
78  *==============================================================================
79  *
80  * Implementation
81  *
82  *==============================================================================
83  *
84  * Global functions callable by other modules
85  *
86  *------------------------------------------------------------------------------
87  */
88 GLOBAL char *skyio_degToDmsStr(char destStr[],
89  size_t destStrSize,
90  double angle_deg,
91  unsigned decimals)
92 /*! Routine to take an angle in degrees and return a string of the form
93  [±]DDD°MM′SS.sss″
94  correctly rounding according to the number of decimal places to be shown.
95  The angle is assumed to be within the range (-360.0, 360.0). Angles which
96  round to ±360° will be written out as 0°.
97  \returns Pointer to \a destStr
98  \param[out] destStr Destination character string
99  \param[in] destStrSize Size of destination string (max available length + 1)
100  \param[in] angle_deg The angle to be written out (degrees).
101  Valid range:(-360, 360); larger numbers may well be
102  written OK, but there is a risk of overflowing an
103  intermediate variable. No error will be detected,
104  just a wrong answer will be written.
105  \param[in] decimals Number of digits after the decimal point to display.
106  Valid range: [0,9] (or [0,3] if long int is only a
107  32-bit number); numbers outside this range will be
108  clamped to this range.
109 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
110 {
111  const unsigned minLen = 8 + (sizeof(cDegree) - 1) + (sizeof(cMinute) - 1)
112  + (sizeof(cSecond) - 1);
113 
114  long int angle_asxd; // angle_deg, converted to arcseconds x 10^decimals
115  unsigned reqLen; // required length of string to fit the output
116  unsigned i, j;
117  int roundup;
118  ldiv_t q;
119 
120  if (sizeof(angle_asxd) > 4) {
121  // Size of angle_asxd is not an issue for us.
122  if (decimals > 9) { decimals = 9; }
123  } else {
124  // angle_asxd is only a 32-bit variable. Make sure we don't overflow it.
125  if (decimals > 3) { decimals = 3; }
126  }
127 
128  // How many chars will we need to write the angle, including decimal point?
129  reqLen = minLen + decimals + (decimals > 0 ? 1 : 0);
130  // Make sure caller has supplied a big enough string to hold all the digits
131  // that they have requested
132  REQUIRE(destStrSize > reqLen);
133 
134  roundup = 1;
135  for (i = 0; i < decimals; i++) {
136  roundup *= 10;
137  }
138  angle_asxd = lround(angle_deg * 3600.0 * roundup);
139  if (angle_deg < 0) {
140  angle_asxd = -angle_asxd;
141  }
142 
143 
144  i = reqLen;
145  destStr[i] = '\0';
146  // Insert "″"
147  i -= sizeof(cSecond) - 1;
148  memcpy(&destStr[i], cSecond, sizeof(cSecond) - 1);
149  // Arcseconds and any fraction
150  q = ldiv(angle_asxd, 10);
151  destStr[--i] = digits[q.rem];
152  for (j = decimals; j > 0; j--) {
153  if (j == 1) {
154  destStr[--i] = decimalChar;
155  }
156  q = ldiv(q.quot, 10);
157  destStr[--i] = digits[q.rem];
158  }
159  q = ldiv(q.quot, 6);
160  destStr[--i] = digits[q.rem];
161 
162  // Insert "′"
163  i -= sizeof(cMinute) - 1;
164  memcpy(&destStr[i], cMinute, sizeof(cMinute) - 1);
165  // Arcminutes
166  q = ldiv(q.quot, 10);
167  destStr[--i] = digits[q.rem];
168  q = ldiv(q.quot, 6);
169  destStr[--i] = digits[q.rem];
170 
171  // Insert "°"
172  i -= sizeof(cDegree) - 1;
173  memcpy(&destStr[i], cDegree, sizeof(cDegree) - 1);
174  // Degrees
175  if (q.quot == 360) {
176  q.quot = 0;
177  }
178 
179  for (j = 0; (j < 1) || (q.quot != 0); j++) {
180  q = ldiv(q.quot, 10);
181  destStr[--i] = digits[q.rem];
182  }
183  destStr[--i] = (angle_deg < 0.0) ? '-' : '+';
184  while (i > 0) {
185  destStr[--i] = ' ';
186  }
187 
188  return destStr;
189 }
190 
191 
192 
193 GLOBAL char *skyio_hrsToHmsStr(char destStr[],
194  size_t destStrSize,
195  double angle_h,
196  unsigned decimals)
197 /*! Routine to take an angle in hours and return a string in hours, minutes and
198  seconds form - "±HH:MM:SS.sss" -
199  correctly rounding according to the number of decimal places to be shown.
200  The angle is assumed to be within the range (-24.0, 24.0). Angles which
201  round to ±24:00:00 will be written out as 0:00:00.
202  \returns Pointer to \a destStr
203  \param[out] destStr Destination character string
204  \param[in] destStrSize Size of destination string (max available length + 1)
205  \param[in] angle_h The angle to be written out (hours).
206  Valid range:(-24.0, 24.0); larger numbers may well be
207  written OK, but there is a risk of overflowing an
208  intermediate variable. No error will be detected,
209  just a wrong answer will be written.
210  \param[in] decimals Number of digits after the decimal point to display.
211  Valid range: [0,9] (or [0,3] if long int is only a
212  32-bit number); numbers outside this range will be
213  clamped to this range.
214 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
215 {
216  long int angle_sxd; // angle_h, converted to seconds x 10^decimals
217  unsigned i, j;
218  unsigned reqLen; // required length of string to fit the output
219  int roundup;
220  ldiv_t q;
221 
222  if (sizeof(angle_sxd) > 4) {
223  // Size of angle_sxd is not an issue for us.
224  if (decimals > 9) { decimals = 9; }
225  } else {
226  // angle_sxd is only a 32-bit variable. Make sure we don't overflow it.
227  if (decimals > 4) { decimals = 4; }
228  }
229 
230  // How many chars will we need to write the angle, including decimal point?
231  reqLen = 9 + decimals + (decimals > 0 ? 1 : 0);
232  // Make sure caller has supplied a big enough string to hold all the digits
233  // that they have requested
234  REQUIRE(destStrSize > reqLen);
235 
236 
237  roundup = 1;
238  for (i = 0; i < decimals; i++) {
239  roundup *= 10;
240  }
241  angle_sxd = lround(angle_h * 3600.0 * roundup);
242  if (angle_h < 0) {
243  angle_sxd = -angle_sxd;
244  }
245 
246  i = reqLen;
247  destStr[i] = '\0';
248  // Seconds and any fraction
249  q = ldiv(angle_sxd, 10);
250  destStr[--i] = digits[q.rem];
251  for (j = decimals; j > 0; j--) {
252  if (j == 1) {
253  destStr[--i] = decimalChar;
254  }
255  q = ldiv(q.quot, 10);
256  destStr[--i] = digits[q.rem];
257  }
258  q = ldiv(q.quot, 6);
259  destStr[--i] = digits[q.rem];
260 
261  destStr[--i] = ':';
262  // Minutes
263  q = ldiv(q.quot, 10);
264  destStr[--i] = digits[q.rem];
265  q = ldiv(q.quot, 6);
266  destStr[--i] = digits[q.rem];
267 
268  destStr[--i] = ':';
269  // Hours
270  if (q.quot == 24) {
271  q.quot = 0;
272  }
273 
274  for (j = 0; j < 2; j++) {
275  q = ldiv(q.quot, 10);
276  destStr[--i] = digits[q.rem];
277  }
278 
279  destStr[--i] = (angle_h < 0.0) ? '-' : '+';
280 
281  ENSURE(i == 0); // If not, we have stuffed up filling the string
282 
283  return destStr;
284 }
285 
286 
287 
288 GLOBAL double skyio_sxStrToAng(const char angleStr[],
289  const char **endPtr,
290  int *error)
291 /*! Convert a string containing an angle (or a time) in sexagesimal format to
292  the angle's value. The field containing the angle consists of either one
293  subfield (decimal degrees or hours), two subfields (degrees and arcminutes
294  or hours and minutes) three subfields (degrees, arcminutes and arcseconds or
295  hours minutes and seconds). The location of the decimal point (if any) is
296  what determines how many subfields are present. The following are all
297  examples of valid angles (or times):
298  - 21.625 or +21.625°
299  - -21 37.5 or 21:37.5 or -21°37.5′ or 21h37.5m
300  - +21 37 30 or 21:37:30.0 or -21°37′30.0″
301 
302  If the string contains degrees, minutes and seconds, the result will be in
303  degrees. If the string contains hours, minutes and seconds, the result will
304  be in hours.
305 
306  The routine will accept almost anything appearing between the numbers,
307  such as °, ′, & ″; or d, ' & "; or h, m, & s; or colons or spaces or tabs.
308  These are ignored and are not checked for whether they make sense or not.
309  \returns The angle or time
310  \param[in] angleStr The string of text containing the angle
311  \param[out] endPtr A pointer to the end of that part of \a angleStr that was
312  read to obtain the number. This may be pointing to some
313  white space between the number just decoded and further
314  data, or it may be the end of the string. A NULL
315  pointer may be passed to this parameter if you do not
316  need this value.
317  \param[out] error What errors were detected during decoding?
318  This is set to NO_ANGLE (-1) if no value was found in the
319  string.
320  It is set to INVALID_ANGLE (-2) if the minutes or
321  seconds fields are outside the range [0.0, 60.0).
322  A NULL pointer may be passed to this parameter if you
323  do not care about the conversion status.
324 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
325 {
326  const char *ch; // Step through the string
327  bool isNegative = false; // Have we encountered a -ve sign?
328  bool pointFound = false; // Have we encountered a decimal point?
329  bool done = false; // Have we finished decoding the string?
330  double divideBy;
331  double frac; // value of digits that follow decimal point
332  int status; // error code
333  /* Hold the results of decoding in separate array members for degrees/hours,
334  minutes and seconds. Put an invalid value into one of them, so that we
335  can later detect the case when angleStr contains no decodable value. */
336  double results[4] = {0.0, -1.0, 0.0, 0.0};
337  double *res = results; // point to relevant member of results[]
338 
339  enum {
340  stStarting, stDegHr, stSeparator1, stMins,
341  stSeparator2, stSecs, stEnding
342  } decodeState = stStarting; // state variable - stage of decoding
343 
344  for (ch = angleStr; *ch != '\0'; ch++) {
345  switch (decodeState) {
346  case stStarting:
347  if (*ch == '-') { isNegative = true; }
348  NOBREAK;
349  case stSeparator1:
350  case stSeparator2:
351  if (isdigit(*ch)) {
352  *(++res) = (double)(*ch - '0');
353  decodeState++;
354  }
355  break;
356 
357  case stDegHr:
358  case stMins:
359  case stSecs:
360  if (*ch == decimalChar) {
361  pointFound = true;
362  divideBy = 1.0;
363 
364  } else if (isdigit(*ch)) {
365  if (pointFound) {
366  divideBy *= 10.0;
367  frac = (double)(*ch - '0') / divideBy;
368  *res += frac;
369  } else {
370  *res = (10.0 * *res) + (double)(*ch - '0');
371  }
372 
373  } else {
374  // Not a digit or point, so we have reached the end of the field
375  if ((pointFound) || (decodeState == stSecs)) {
376  /* This must be the last field we will decode */
377  if (isblank(*ch)) {
378  done = true; // exit now
379  } else if (*ch == fieldSeparator) {
380  ch++; // if a separator, move to char
381  done = true; // after the separator and exit
382  } else {
383  decodeState = stEnding; // skip non-blanks, then exit
384  }
385  } else {
386  decodeState++;
387  }
388  }
389  break;
390 
391  case stEnding:
392  /* Skip over any trailing non-blank chars.
393  Stop when we find white space */
394  if (isblank(*ch)) {
395  done = true;
396  }
397  break;
398 
399  default:
400  break;
401  }
402 
403  if (done) {
404  /* exit the loop without incrementing our character pointer ch */
405  break;
406  }
407  }
408  status = 0;
409  if (results[1] < 0.0) {
410  /* There was no number in angleStr. Return zero and an error code. */
411  results[1] = 0.0;
412  status = NO_ANGLE;
413  }
414  if (results[3] >= 60.0) { status = INVALID_ANGLE; }
415  if (results[2] >= 60.0) { status = INVALID_ANGLE; }
416  if (error != NULL) { *error = status; }
417  if (endPtr != NULL) { *endPtr = ch; }
418 
419  results[0] = results[1] + results[2] / 60.0 + results[3] / 3600.0;
420  if (isNegative) { results[0] = -results[0]; }
421  return results[0];
422 }
423 
424 
425 
426 GLOBAL void skyio_printJ2kd(double j2kd)
427 /*! Write out a J2KD as a calendar date and time. The date and time are written
428  in ISO format, separated by a space. Time is written to three decimal places
429  of seconds. No newline is written at the end.
430  \param[in] j2kd The date/time in J2KD format.
431 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
432 {
433  int y, m, d, h, mins;
434  double s;
435 
436  sky_j2kdToCalTime(j2kd, &y, &m, &d, &h, &mins, &s);
437  printf("%04d-%02d-%02d %02d:%02d:%06.3f", y, m, d, h, mins, s);
438 }
439 
440 
441 /*
442  *------------------------------------------------------------------------------
443  *
444  * Local functions (not called from other modules)
445  *
446  *------------------------------------------------------------------------------
447  */
448 /*- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
449 
450 
451 
452 
general.h
general.h - definitions of general use to (standard) C programs
sky_j2kdToCalTime
void sky_j2kdToCalTime(double j2k_d, int *year, int *month, int *day, int *hour, int *minute, double *second)
This procedure converts the integral part of the date in "J2KD" form to a calendar date and the fract...
Definition: sky-time.c:321
skyio.h
skyio.h - output and formatting routines and a read routine
REQUIRE
#define REQUIRE(test_)
Check preconditions.
Definition: general.h:273
LOCAL
#define LOCAL
C has some very silly default behaviours.
Definition: general.h:133
skyio_hrsToHmsStr
char * skyio_hrsToHmsStr(char destStr[], size_t destStrSize, double angle_h, unsigned decimals)
Routine to take an angle in hours and return a string in hours, minutes and seconds form - "±HH:MM:SS...
Definition: skyio.c:193
GLOBAL
#define GLOBAL
See above.
Definition: general.h:134
skyio_sxStrToAng
double skyio_sxStrToAng(const char angleStr[], const char **endPtr, int *error)
Convert a string containing an angle (or a time) in sexagesimal format to the angle's value.
Definition: skyio.c:288
ENSURE
#define ENSURE(test_)
Check post-conditions.
Definition: general.h:279
DEFINE_THIS_FILE
#define DEFINE_THIS_FILE
Leave DEFINE_THIS_FILE undefined.
Definition: general.h:252
sky.h
sky.h - structures and routines for astronomical observing & tracking
skyio_printJ2kd
void skyio_printJ2kd(double j2kd)
Write out a J2KD as a calendar date and time.
Definition: skyio.c:426
skyio_degToDmsStr
char * skyio_degToDmsStr(char destStr[], size_t destStrSize, double angle_deg, unsigned decimals)
Routine to take an angle in degrees and return a string of the form [±]DDD°MM′SS.sss″ correctly round...
Definition: skyio.c:88
NOBREAK
#define NOBREAK
Show reader that 'break' was not omitted by accident.
Definition: general.h:143