Coverage Report

Created: 2025-08-26 07:08

/src/PROJ/src/conversions/unitconvert.cpp
Line
Count
Source (jump to first uncovered line)
1
/***********************************************************************
2
3
            Unit conversion pseudo-projection for use with
4
                       transformation pipelines.
5
6
                      Kristian Evers, 2017-05-16
7
8
************************************************************************
9
10
A pseudo-projection that can be used to convert units of input and
11
output data. Primarily useful in pipelines.
12
13
Unit conversion is performed by means of a pivot unit. The pivot unit
14
for distance units are the meter and for time we use the modified julian
15
date. A time unit conversion is performed like
16
17
    Unit A -> Modified Julian date -> Unit B
18
19
distance units are converted in the same manner, with meter being the
20
central unit.
21
22
The modified Julian date is chosen as the pivot unit since it has a
23
fairly high precision, goes sufficiently long backwards in time, has no
24
danger of hitting the upper limit in the near future and it is a fairly
25
common time unit in astronomy and geodesy. Note that we are using the
26
Julian date and not day. The difference being that the latter is defined
27
as an integer and is thus limited to days in resolution. This approach
28
has been extended wherever it makes sense, e.g. the GPS week unit also
29
has a fractional part that makes it possible to determine the day, hour
30
and minute of an observation.
31
32
In- and output units are controlled with the parameters
33
34
    +xy_in, +xy_out, +z_in, +z_out, +t_in and +t_out
35
36
where xy denotes horizontal units, z vertical units and t time units.
37
38
************************************************************************
39
40
Kristian Evers, kreve@sdfe.dk, 2017-05-09
41
Last update: 2017-05-16
42
43
************************************************************************
44
* Copyright (c) 2017, Kristian Evers / SDFE
45
*
46
* Permission is hereby granted, free of charge, to any person obtaining a
47
* copy of this software and associated documentation files (the "Software"),
48
* to deal in the Software without restriction, including without limitation
49
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
50
* and/or sell copies of the Software, and to permit persons to whom the
51
* Software is furnished to do so, subject to the following conditions:
52
*
53
* The above copyright notice and this permission notice shall be included
54
* in all copies or substantial portions of the Software.
55
*
56
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
57
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
58
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
59
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
60
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
61
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
62
* DEALINGS IN THE SOFTWARE.
63
*
64
***********************************************************************/
65
66
#include <errno.h>
67
#include <math.h>
68
#include <string.h>
69
#include <time.h>
70
71
#include "proj_internal.h"
72
#include <math.h>
73
74
PROJ_HEAD(unitconvert, "Unit conversion");
75
76
typedef double (*tconvert)(double);
77
78
namespace { // anonymous namespace
79
struct TIME_UNITS {
80
    const char *id;   /* units keyword */
81
    tconvert t_in;    /* unit -> mod. julian date function pointer */
82
    tconvert t_out;   /* mod. julian date > unit function pointer */
83
    const char *name; /* comments */
84
};
85
} // anonymous namespace
86
87
namespace { // anonymous namespace
88
struct pj_opaque_unitconvert {
89
    int t_in_id;      /* time unit id for the time input unit   */
90
    int t_out_id;     /* time unit id for the time output unit  */
91
    double xy_factor; /* unit conversion factor for horizontal components */
92
    double z_factor;  /* unit conversion factor for vertical components */
93
};
94
} // anonymous namespace
95
96
/***********************************************************************/
97
0
static int is_leap_year(long year) {
98
    /***********************************************************************/
99
0
    return ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0);
100
0
}
101
102
/***********************************************************************/
103
0
static int days_in_year(long year) {
104
    /***********************************************************************/
105
0
    return is_leap_year(year) ? 366 : 365;
106
0
}
107
108
/***********************************************************************/
109
0
static unsigned int days_in_month(unsigned long year, unsigned long month) {
110
    /***********************************************************************/
111
0
    const unsigned int month_table[] = {31, 28, 31, 30, 31, 30,
112
0
                                        31, 31, 30, 31, 30, 31};
113
0
    unsigned int days;
114
115
0
    if (month > 12)
116
0
        month = 12;
117
0
    if (month == 0)
118
0
        month = 1;
119
120
0
    days = month_table[month - 1];
121
0
    if (is_leap_year(year) && month == 2)
122
0
        days++;
123
124
0
    return days;
125
0
}
126
127
/***********************************************************************/
128
static int daynumber_in_year(unsigned long year, unsigned long month,
129
0
                             unsigned long day) {
130
    /***********************************************************************/
131
0
    unsigned int daynumber = 0, i;
132
133
0
    if (month > 12)
134
0
        month = 12;
135
0
    if (month == 0)
136
0
        month = 1;
137
0
    if (day > days_in_month(year, month))
138
0
        day = days_in_month(year, month);
139
140
0
    for (i = 1; i < month; i++)
141
0
        daynumber += days_in_month(year, i);
142
143
0
    daynumber += day;
144
145
0
    return daynumber;
146
0
}
147
148
/***********************************************************************/
149
0
static double mjd_to_mjd(double mjd) {
150
    /***********************************************************************
151
        Modified julian date no-op function.
152
153
        The Julian date is defined as (fractional) days since midnight
154
        on 16th of November in 1858.
155
    ************************************************************************/
156
0
    return mjd;
157
0
}
158
159
/***********************************************************************/
160
0
static double decimalyear_to_mjd(double decimalyear) {
161
    /***********************************************************************
162
        Epoch of modified julian date is 1858-11-16 00:00
163
    ************************************************************************/
164
0
    long year;
165
0
    double fractional_year;
166
0
    double mjd;
167
168
    // Written this way to deal with NaN input
169
0
    if (!(decimalyear >= -10000 && decimalyear <= 10000))
170
0
        return 0;
171
172
0
    year = lround(floor(decimalyear));
173
0
    fractional_year = decimalyear - year;
174
0
    mjd = (year - 1859) * 365 + 14 + 31;
175
0
    mjd += (double)fractional_year * (double)days_in_year(year);
176
177
    /* take care of leap days */
178
0
    year--;
179
0
    for (; year > 1858; year--)
180
0
        if (is_leap_year(year))
181
0
            mjd++;
182
183
0
    return mjd;
184
0
}
185
186
/***********************************************************************/
187
0
static double mjd_to_decimalyear(double mjd) {
188
    /***********************************************************************
189
        Epoch of modified julian date is 1858-11-16 00:00
190
    ************************************************************************/
191
0
    double decimalyear = mjd;
192
0
    double mjd_iter = 14 + 31;
193
0
    int year = 1859;
194
195
    /* a smarter brain than mine could probably to do this more elegantly
196
       - I'll just brute-force my way out of this... */
197
0
    for (; mjd >= mjd_iter; year++) {
198
0
        mjd_iter += days_in_year(year);
199
0
    }
200
0
    year--;
201
0
    mjd_iter -= days_in_year(year);
202
203
0
    decimalyear = year + (mjd - mjd_iter) / days_in_year(year);
204
0
    return decimalyear;
205
0
}
206
207
/***********************************************************************/
208
0
static double gps_week_to_mjd(double gps_week) {
209
    /***********************************************************************
210
        GPS weeks are defined as the number of weeks since January the 6th
211
        1980.
212
213
        Epoch of gps weeks is 1980-01-06 00:00, which in modified Julian
214
        date is 44244.
215
    ************************************************************************/
216
0
    return 44244.0 + gps_week * 7.0;
217
0
}
218
219
/***********************************************************************/
220
0
static double mjd_to_gps_week(double mjd) {
221
    /***********************************************************************
222
        GPS weeks are defined as the number of weeks since January the 6th
223
        1980.
224
225
        Epoch of gps weeks is 1980-01-06 00:00, which in modified Julian
226
        date is 44244.
227
    ************************************************************************/
228
0
    return (mjd - 44244.0) / 7.0;
229
0
}
230
231
/***********************************************************************/
232
0
static double yyyymmdd_to_mjd(double yyyymmdd) {
233
    /************************************************************************
234
        Date given in YYYY-MM-DD format.
235
    ************************************************************************/
236
237
0
    long year = lround(floor(yyyymmdd / 10000));
238
0
    long month = lround(floor((yyyymmdd - year * 10000) / 100));
239
0
    long day = lround(floor(yyyymmdd - year * 10000 - month * 100));
240
0
    double mjd = daynumber_in_year(year, month, day);
241
242
0
    for (year -= 1; year > 1858; year--)
243
0
        mjd += days_in_year(year);
244
245
0
    return mjd + 13 + 31;
246
0
}
247
248
/***********************************************************************/
249
0
static double mjd_to_yyyymmdd(double mjd) {
250
    /************************************************************************
251
        Date returned in YYYY-MM-DD format.
252
    ************************************************************************/
253
0
    unsigned int date_iter = 14 + 31;
254
0
    unsigned int year = 1859, month = 0, day = 0;
255
0
    unsigned int date = (int)lround(mjd);
256
257
0
    for (; date >= date_iter; year++) {
258
0
        date_iter += days_in_year(year);
259
0
    }
260
0
    year--;
261
0
    date_iter -= days_in_year(year);
262
263
0
    for (month = 1; date_iter + days_in_month(year, month) <= date; month++)
264
0
        date_iter += days_in_month(year, month);
265
266
0
    day = date - date_iter + 1;
267
268
0
    return year * 10000.0 + month * 100.0 + day;
269
0
}
270
271
static const struct TIME_UNITS time_units[] = {
272
    {"mjd", mjd_to_mjd, mjd_to_mjd, "Modified julian date"},
273
    {"decimalyear", decimalyear_to_mjd, mjd_to_decimalyear, "Decimal year"},
274
    {"gps_week", gps_week_to_mjd, mjd_to_gps_week, "GPS Week"},
275
    {"yyyymmdd", yyyymmdd_to_mjd, mjd_to_yyyymmdd, "YYYYMMDD date"},
276
    {nullptr, nullptr, nullptr, nullptr}};
277
278
/***********************************************************************/
279
2.38M
static PJ_XY forward_2d(PJ_LP lp, PJ *P) {
280
    /************************************************************************
281
        Forward unit conversions in the plane
282
    ************************************************************************/
283
2.38M
    struct pj_opaque_unitconvert *Q = (struct pj_opaque_unitconvert *)P->opaque;
284
2.38M
    PJ_COORD point = {{0, 0, 0, 0}};
285
2.38M
    point.lp = lp;
286
287
2.38M
    point.xy.x *= Q->xy_factor;
288
2.38M
    point.xy.y *= Q->xy_factor;
289
290
2.38M
    return point.xy;
291
2.38M
}
292
293
/***********************************************************************/
294
420
static PJ_LP reverse_2d(PJ_XY xy, PJ *P) {
295
    /************************************************************************
296
        Reverse unit conversions in the plane
297
    ************************************************************************/
298
420
    struct pj_opaque_unitconvert *Q = (struct pj_opaque_unitconvert *)P->opaque;
299
420
    PJ_COORD point = {{0, 0, 0, 0}};
300
420
    point.xy = xy;
301
302
420
    point.xy.x /= Q->xy_factor;
303
420
    point.xy.y /= Q->xy_factor;
304
305
420
    return point.lp;
306
420
}
307
308
/***********************************************************************/
309
2.38M
static PJ_XYZ forward_3d(PJ_LPZ lpz, PJ *P) {
310
    /************************************************************************
311
        Forward unit conversions the vertical component
312
    ************************************************************************/
313
2.38M
    struct pj_opaque_unitconvert *Q = (struct pj_opaque_unitconvert *)P->opaque;
314
2.38M
    PJ_COORD point = {{0, 0, 0, 0}};
315
2.38M
    point.lpz = lpz;
316
317
    /* take care of the horizontal components in the 2D function */
318
319
    // Assigning in 2 steps avoids cppcheck warning
320
    // "Overlapping read/write of union is undefined behavior"
321
    // Cf https://github.com/OSGeo/PROJ/pull/3527#pullrequestreview-1233332710
322
2.38M
    const auto xy = forward_2d(point.lp, P);
323
2.38M
    point.xy = xy;
324
325
2.38M
    point.xyz.z *= Q->z_factor;
326
327
2.38M
    return point.xyz;
328
2.38M
}
329
330
/***********************************************************************/
331
420
static PJ_LPZ reverse_3d(PJ_XYZ xyz, PJ *P) {
332
    /************************************************************************
333
        Reverse unit conversions the vertical component
334
    ************************************************************************/
335
420
    struct pj_opaque_unitconvert *Q = (struct pj_opaque_unitconvert *)P->opaque;
336
420
    PJ_COORD point = {{0, 0, 0, 0}};
337
420
    point.xyz = xyz;
338
339
    /* take care of the horizontal components in the 2D function */
340
341
    // Assigning in 2 steps avoids cppcheck warning
342
    // "Overlapping read/write of union is undefined behavior"
343
    // Cf https://github.com/OSGeo/PROJ/pull/3527#pullrequestreview-1233332710
344
420
    const auto lp = reverse_2d(point.xy, P);
345
420
    point.lp = lp;
346
347
420
    point.xyz.z /= Q->z_factor;
348
349
420
    return point.lpz;
350
420
}
351
352
/***********************************************************************/
353
2.38M
static void forward_4d(PJ_COORD &coo, PJ *P) {
354
    /************************************************************************
355
        Forward conversion of time units
356
    ************************************************************************/
357
2.38M
    struct pj_opaque_unitconvert *Q = (struct pj_opaque_unitconvert *)P->opaque;
358
359
    /* delegate unit conversion of physical dimensions to the 3D function */
360
361
    // Assigning in 2 steps avoids cppcheck warning
362
    // "Overlapping read/write of union is undefined behavior"
363
    // Cf https://github.com/OSGeo/PROJ/pull/3527#pullrequestreview-1233332710
364
2.38M
    const auto xyz = forward_3d(coo.lpz, P);
365
2.38M
    coo.xyz = xyz;
366
367
2.38M
    if (Q->t_in_id >= 0)
368
0
        coo.xyzt.t = time_units[Q->t_in_id].t_in(coo.xyzt.t);
369
2.38M
    if (Q->t_out_id >= 0)
370
0
        coo.xyzt.t = time_units[Q->t_out_id].t_out(coo.xyzt.t);
371
2.38M
}
372
373
/***********************************************************************/
374
420
static void reverse_4d(PJ_COORD &coo, PJ *P) {
375
    /************************************************************************
376
        Reverse conversion of time units
377
    ************************************************************************/
378
420
    struct pj_opaque_unitconvert *Q = (struct pj_opaque_unitconvert *)P->opaque;
379
380
    /* delegate unit conversion of physical dimensions to the 3D function */
381
382
    // Assigning in 2 steps avoids cppcheck warning
383
    // "Overlapping read/write of union is undefined behavior"
384
    // Cf https://github.com/OSGeo/PROJ/pull/3527#pullrequestreview-1233332710
385
420
    const auto lpz = reverse_3d(coo.xyz, P);
386
420
    coo.lpz = lpz;
387
388
420
    if (Q->t_out_id >= 0)
389
0
        coo.xyzt.t = time_units[Q->t_out_id].t_in(coo.xyzt.t);
390
420
    if (Q->t_in_id >= 0)
391
0
        coo.xyzt.t = time_units[Q->t_in_id].t_out(coo.xyzt.t);
392
420
}
393
394
/***********************************************************************/
395
static double get_unit_conversion_factor(const char *name, int *p_is_linear,
396
40.0k
                                         const char **p_normalized_name) {
397
    /***********************************************************************/
398
40.0k
    int i;
399
40.0k
    const char *s;
400
40.0k
    const PJ_UNITS *units = pj_list_linear_units();
401
402
    /* Try first with linear units */
403
808k
    for (i = 0; (s = units[i].id); ++i) {
404
772k
        if (strcmp(s, name) == 0) {
405
3.93k
            if (p_normalized_name) {
406
3.93k
                *p_normalized_name = units[i].name;
407
3.93k
            }
408
3.93k
            if (p_is_linear) {
409
3.93k
                *p_is_linear = 1;
410
3.93k
            }
411
3.93k
            return units[i].factor;
412
3.93k
        }
413
772k
    }
414
415
    /* And then angular units */
416
36.1k
    units = pj_list_angular_units();
417
55.4k
    for (i = 0; (s = units[i].id); ++i) {
418
54.9k
        if (strcmp(s, name) == 0) {
419
35.6k
            if (p_normalized_name) {
420
35.6k
                *p_normalized_name = units[i].name;
421
35.6k
            }
422
35.6k
            if (p_is_linear) {
423
35.6k
                *p_is_linear = 0;
424
35.6k
            }
425
35.6k
            return units[i].factor;
426
35.6k
        }
427
54.9k
    }
428
526
    if (p_normalized_name) {
429
526
        *p_normalized_name = nullptr;
430
526
    }
431
526
    if (p_is_linear) {
432
526
        *p_is_linear = -1;
433
526
    }
434
526
    return 0.0;
435
36.1k
}
436
437
/***********************************************************************/
438
19.0k
PJ *PJ_CONVERSION(unitconvert, 0) {
439
    /***********************************************************************/
440
19.0k
    struct pj_opaque_unitconvert *Q =
441
19.0k
        static_cast<struct pj_opaque_unitconvert *>(
442
19.0k
            calloc(1, sizeof(struct pj_opaque_unitconvert)));
443
19.0k
    const char *s, *name;
444
19.0k
    int i;
445
19.0k
    double f;
446
19.0k
    int xy_in_is_linear = -1;  /* unknown */
447
19.0k
    int xy_out_is_linear = -1; /* unknown */
448
19.0k
    int z_in_is_linear = -1;   /* unknown */
449
19.0k
    int z_out_is_linear = -1;  /* unknown */
450
451
19.0k
    if (nullptr == Q)
452
0
        return pj_default_destructor(P, PROJ_ERR_OTHER /*ENOMEM*/);
453
19.0k
    P->opaque = (void *)Q;
454
455
19.0k
    P->fwd4d = forward_4d;
456
19.0k
    P->inv4d = reverse_4d;
457
19.0k
    P->fwd3d = forward_3d;
458
19.0k
    P->inv3d = reverse_3d;
459
19.0k
    P->fwd = forward_2d;
460
19.0k
    P->inv = reverse_2d;
461
462
19.0k
    P->left = PJ_IO_UNITS_WHATEVER;
463
19.0k
    P->right = PJ_IO_UNITS_WHATEVER;
464
19.0k
    P->skip_fwd_prepare = 1;
465
19.0k
    P->skip_inv_prepare = 1;
466
467
    /* if no time input/output unit is specified we can skip them */
468
19.0k
    Q->t_in_id = -1;
469
19.0k
    Q->t_out_id = -1;
470
471
19.0k
    Q->xy_factor = 1.0;
472
19.0k
    Q->z_factor = 1.0;
473
474
19.0k
    if ((name = pj_param(P->ctx, P->params, "sxy_in").s) != nullptr) {
475
18.5k
        const char *normalized_name = nullptr;
476
18.5k
        f = get_unit_conversion_factor(name, &xy_in_is_linear,
477
18.5k
                                       &normalized_name);
478
18.5k
        if (f != 0.0) {
479
18.4k
            proj_log_trace(P, "xy_in unit: %s", normalized_name);
480
18.4k
        } else {
481
126
            f = pj_param(P->ctx, P->params, "dxy_in").f;
482
126
            if (f == 0.0 || 1.0 / f == 0.0) {
483
47
                proj_log_error(P, _("unknown xy_in unit"));
484
47
                return pj_default_destructor(
485
47
                    P, PROJ_ERR_INVALID_OP_ILLEGAL_ARG_VALUE);
486
47
            }
487
126
        }
488
18.5k
        Q->xy_factor = f;
489
18.5k
        if (normalized_name != nullptr) {
490
18.4k
            if (strcmp(normalized_name, "Radian") == 0)
491
10.3k
                P->left = PJ_IO_UNITS_RADIANS;
492
18.4k
            if (strcmp(normalized_name, "Degree") == 0)
493
7.51k
                P->left = PJ_IO_UNITS_DEGREES;
494
18.4k
        }
495
18.5k
    }
496
497
19.0k
    if ((name = pj_param(P->ctx, P->params, "sxy_out").s) != nullptr) {
498
18.5k
        const char *normalized_name = nullptr;
499
18.5k
        f = get_unit_conversion_factor(name, &xy_out_is_linear,
500
18.5k
                                       &normalized_name);
501
18.5k
        if (f != 0.0) {
502
18.3k
            proj_log_trace(P, "xy_out unit: %s", normalized_name);
503
18.3k
        } else {
504
151
            f = pj_param(P->ctx, P->params, "dxy_out").f;
505
151
            if (f == 0.0 || 1.0 / f == 0.0) {
506
121
                proj_log_error(P, _("unknown xy_out unit"));
507
121
                return pj_default_destructor(
508
121
                    P, PROJ_ERR_INVALID_OP_ILLEGAL_ARG_VALUE);
509
121
            }
510
151
        }
511
18.4k
        Q->xy_factor /= f;
512
18.4k
        if (normalized_name != nullptr) {
513
18.3k
            if (strcmp(normalized_name, "Radian") == 0)
514
7.55k
                P->right = PJ_IO_UNITS_RADIANS;
515
18.3k
            if (strcmp(normalized_name, "Degree") == 0)
516
10.2k
                P->right = PJ_IO_UNITS_DEGREES;
517
18.3k
        }
518
18.4k
    }
519
520
18.9k
    if (xy_in_is_linear >= 0 && xy_out_is_linear >= 0 &&
521
18.9k
        xy_in_is_linear != xy_out_is_linear) {
522
2
        proj_log_error(P, _("inconsistent unit type between xy_in and xy_out"));
523
2
        return pj_default_destructor(P, PROJ_ERR_INVALID_OP_ILLEGAL_ARG_VALUE);
524
2
    }
525
526
18.8k
    if ((name = pj_param(P->ctx, P->params, "sz_in").s) != nullptr) {
527
1.47k
        const char *normalized_name = nullptr;
528
1.47k
        f = get_unit_conversion_factor(name, &z_in_is_linear, &normalized_name);
529
1.47k
        if (f != 0.0) {
530
1.37k
            proj_log_trace(P, "z_in unit: %s", normalized_name);
531
1.37k
        } else {
532
94
            f = pj_param(P->ctx, P->params, "dz_in").f;
533
94
            if (f == 0.0 || 1.0 / f == 0.0) {
534
11
                proj_log_error(P, _("unknown z_in unit"));
535
11
                return pj_default_destructor(
536
11
                    P, PROJ_ERR_INVALID_OP_ILLEGAL_ARG_VALUE);
537
11
            }
538
94
        }
539
1.45k
        Q->z_factor = f;
540
1.45k
    }
541
542
18.8k
    if ((name = pj_param(P->ctx, P->params, "sz_out").s) != nullptr) {
543
1.47k
        const char *normalized_name = nullptr;
544
1.47k
        f = get_unit_conversion_factor(name, &z_out_is_linear,
545
1.47k
                                       &normalized_name);
546
1.47k
        if (f != 0.0) {
547
1.31k
            proj_log_trace(P, "z_out unit: %s", normalized_name);
548
1.31k
        } else {
549
155
            f = pj_param(P->ctx, P->params, "dz_out").f;
550
155
            if (f == 0.0 || 1.0 / f == 0.0) {
551
112
                proj_log_error(P, _("unknown z_out unit"));
552
112
                return pj_default_destructor(
553
112
                    P, PROJ_ERR_INVALID_OP_ILLEGAL_ARG_VALUE);
554
112
            }
555
155
        }
556
1.36k
        Q->z_factor /= f;
557
1.36k
    }
558
559
18.7k
    if (z_in_is_linear >= 0 && z_out_is_linear >= 0 &&
560
18.7k
        z_in_is_linear != z_out_is_linear) {
561
0
        proj_log_error(P, _("inconsistent unit type between z_in and z_out"));
562
0
        return pj_default_destructor(P, PROJ_ERR_INVALID_OP_ILLEGAL_ARG_VALUE);
563
0
    }
564
565
18.7k
    if ((name = pj_param(P->ctx, P->params, "st_in").s) != nullptr) {
566
38
        for (i = 0; (s = time_units[i].id) && strcmp(name, s); ++i)
567
29
            ;
568
569
9
        if (!s) {
570
2
            proj_log_error(P, _("unknown t_in unit"));
571
2
            return pj_default_destructor(P,
572
2
                                         PROJ_ERR_INVALID_OP_ILLEGAL_ARG_VALUE);
573
2
        }
574
575
7
        Q->t_in_id = i;
576
7
        proj_log_trace(P, "t_in unit: %s", time_units[i].name);
577
7
    }
578
579
18.7k
    s = nullptr;
580
18.7k
    if ((name = pj_param(P->ctx, P->params, "st_out").s) != nullptr) {
581
91
        for (i = 0; (s = time_units[i].id) && strcmp(name, s); ++i)
582
64
            ;
583
584
27
        if (!s) {
585
4
            proj_log_error(P, _("unknown t_out unit"));
586
4
            return pj_default_destructor(P,
587
4
                                         PROJ_ERR_INVALID_OP_ILLEGAL_ARG_VALUE);
588
4
        }
589
590
23
        Q->t_out_id = i;
591
23
        proj_log_trace(P, "t_out unit: %s", time_units[i].name);
592
23
    }
593
594
18.7k
    return P;
595
18.7k
}