Coverage Report

Created: 2025-11-09 06:51

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/PROJ/src/conversions/unitconvert.cpp
Line
Count
Source
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
1.55M
static PJ_XY forward_2d(PJ_LP lp, PJ *P) {
280
    /************************************************************************
281
        Forward unit conversions in the plane
282
    ************************************************************************/
283
1.55M
    struct pj_opaque_unitconvert *Q = (struct pj_opaque_unitconvert *)P->opaque;
284
1.55M
    PJ_COORD point = {{0, 0, 0, 0}};
285
1.55M
    point.lp = lp;
286
287
1.55M
    point.xy.x *= Q->xy_factor;
288
1.55M
    point.xy.y *= Q->xy_factor;
289
290
1.55M
    return point.xy;
291
1.55M
}
292
293
/***********************************************************************/
294
168
static PJ_LP reverse_2d(PJ_XY xy, PJ *P) {
295
    /************************************************************************
296
        Reverse unit conversions in the plane
297
    ************************************************************************/
298
168
    struct pj_opaque_unitconvert *Q = (struct pj_opaque_unitconvert *)P->opaque;
299
168
    PJ_COORD point = {{0, 0, 0, 0}};
300
168
    point.xy = xy;
301
302
168
    point.xy.x /= Q->xy_factor;
303
168
    point.xy.y /= Q->xy_factor;
304
305
168
    return point.lp;
306
168
}
307
308
/***********************************************************************/
309
1.55M
static PJ_XYZ forward_3d(PJ_LPZ lpz, PJ *P) {
310
    /************************************************************************
311
        Forward unit conversions the vertical component
312
    ************************************************************************/
313
1.55M
    struct pj_opaque_unitconvert *Q = (struct pj_opaque_unitconvert *)P->opaque;
314
1.55M
    PJ_COORD point = {{0, 0, 0, 0}};
315
1.55M
    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
1.55M
    const auto xy = forward_2d(point.lp, P);
323
1.55M
    point.xy = xy;
324
325
1.55M
    point.xyz.z *= Q->z_factor;
326
327
1.55M
    return point.xyz;
328
1.55M
}
329
330
/***********************************************************************/
331
168
static PJ_LPZ reverse_3d(PJ_XYZ xyz, PJ *P) {
332
    /************************************************************************
333
        Reverse unit conversions the vertical component
334
    ************************************************************************/
335
168
    struct pj_opaque_unitconvert *Q = (struct pj_opaque_unitconvert *)P->opaque;
336
168
    PJ_COORD point = {{0, 0, 0, 0}};
337
168
    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
168
    const auto lp = reverse_2d(point.xy, P);
345
168
    point.lp = lp;
346
347
168
    point.xyz.z /= Q->z_factor;
348
349
168
    return point.lpz;
350
168
}
351
352
/***********************************************************************/
353
1.55M
static void forward_4d(PJ_COORD &coo, PJ *P) {
354
    /************************************************************************
355
        Forward conversion of time units
356
    ************************************************************************/
357
1.55M
    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
1.55M
    const auto xyz = forward_3d(coo.lpz, P);
365
1.55M
    coo.xyz = xyz;
366
367
1.55M
    if (Q->t_in_id >= 0)
368
0
        coo.xyzt.t = time_units[Q->t_in_id].t_in(coo.xyzt.t);
369
1.55M
    if (Q->t_out_id >= 0)
370
0
        coo.xyzt.t = time_units[Q->t_out_id].t_out(coo.xyzt.t);
371
1.55M
}
372
373
/***********************************************************************/
374
168
static void reverse_4d(PJ_COORD &coo, PJ *P) {
375
    /************************************************************************
376
        Reverse conversion of time units
377
    ************************************************************************/
378
168
    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
168
    const auto lpz = reverse_3d(coo.xyz, P);
386
168
    coo.lpz = lpz;
387
388
168
    if (Q->t_out_id >= 0)
389
0
        coo.xyzt.t = time_units[Q->t_out_id].t_in(coo.xyzt.t);
390
168
    if (Q->t_in_id >= 0)
391
0
        coo.xyzt.t = time_units[Q->t_in_id].t_out(coo.xyzt.t);
392
168
}
393
394
/***********************************************************************/
395
static double get_unit_conversion_factor(const char *name, int *p_is_linear,
396
25.0k
                                         const char **p_normalized_name) {
397
    /***********************************************************************/
398
25.0k
    int i;
399
25.0k
    const char *s;
400
25.0k
    const PJ_UNITS *units = pj_list_linear_units();
401
402
    /* Try first with linear units */
403
495k
    for (i = 0; (s = units[i].id); ++i) {
404
473k
        if (strcmp(s, name) == 0) {
405
2.93k
            if (p_normalized_name) {
406
2.93k
                *p_normalized_name = units[i].name;
407
2.93k
            }
408
2.93k
            if (p_is_linear) {
409
2.93k
                *p_is_linear = 1;
410
2.93k
            }
411
2.93k
            return units[i].factor;
412
2.93k
        }
413
473k
    }
414
415
    /* And then angular units */
416
22.0k
    units = pj_list_angular_units();
417
34.0k
    for (i = 0; (s = units[i].id); ++i) {
418
33.6k
        if (strcmp(s, name) == 0) {
419
21.6k
            if (p_normalized_name) {
420
21.6k
                *p_normalized_name = units[i].name;
421
21.6k
            }
422
21.6k
            if (p_is_linear) {
423
21.6k
                *p_is_linear = 0;
424
21.6k
            }
425
21.6k
            return units[i].factor;
426
21.6k
        }
427
33.6k
    }
428
397
    if (p_normalized_name) {
429
397
        *p_normalized_name = nullptr;
430
397
    }
431
397
    if (p_is_linear) {
432
397
        *p_is_linear = -1;
433
397
    }
434
397
    return 0.0;
435
22.0k
}
436
437
/***********************************************************************/
438
11.9k
PJ *PJ_CONVERSION(unitconvert, 0) {
439
    /***********************************************************************/
440
11.9k
    struct pj_opaque_unitconvert *Q =
441
11.9k
        static_cast<struct pj_opaque_unitconvert *>(
442
11.9k
            calloc(1, sizeof(struct pj_opaque_unitconvert)));
443
11.9k
    const char *s, *name;
444
11.9k
    int i;
445
11.9k
    double f;
446
11.9k
    int xy_in_is_linear = -1;  /* unknown */
447
11.9k
    int xy_out_is_linear = -1; /* unknown */
448
11.9k
    int z_in_is_linear = -1;   /* unknown */
449
11.9k
    int z_out_is_linear = -1;  /* unknown */
450
451
11.9k
    if (nullptr == Q)
452
0
        return pj_default_destructor(P, PROJ_ERR_OTHER /*ENOMEM*/);
453
11.9k
    P->opaque = (void *)Q;
454
455
11.9k
    P->fwd4d = forward_4d;
456
11.9k
    P->inv4d = reverse_4d;
457
11.9k
    P->fwd3d = forward_3d;
458
11.9k
    P->inv3d = reverse_3d;
459
11.9k
    P->fwd = forward_2d;
460
11.9k
    P->inv = reverse_2d;
461
462
11.9k
    P->left = PJ_IO_UNITS_WHATEVER;
463
11.9k
    P->right = PJ_IO_UNITS_WHATEVER;
464
11.9k
    P->skip_fwd_prepare = 1;
465
11.9k
    P->skip_inv_prepare = 1;
466
467
    /* if no time input/output unit is specified we can skip them */
468
11.9k
    Q->t_in_id = -1;
469
11.9k
    Q->t_out_id = -1;
470
471
11.9k
    Q->xy_factor = 1.0;
472
11.9k
    Q->z_factor = 1.0;
473
474
11.9k
    if ((name = pj_param(P->ctx, P->params, "sxy_in").s) != nullptr) {
475
11.4k
        const char *normalized_name = nullptr;
476
11.4k
        f = get_unit_conversion_factor(name, &xy_in_is_linear,
477
11.4k
                                       &normalized_name);
478
11.4k
        if (f != 0.0) {
479
11.3k
            proj_log_trace(P, "xy_in unit: %s", normalized_name);
480
11.3k
        } else {
481
84
            f = pj_param(P->ctx, P->params, "dxy_in").f;
482
84
            if (f == 0.0 || 1.0 / f == 0.0) {
483
31
                proj_log_error(P, _("unknown xy_in unit"));
484
31
                return pj_default_destructor(
485
31
                    P, PROJ_ERR_INVALID_OP_ILLEGAL_ARG_VALUE);
486
31
            }
487
84
        }
488
11.4k
        Q->xy_factor = f;
489
11.4k
        if (normalized_name != nullptr) {
490
11.3k
            if (strcmp(normalized_name, "Radian") == 0)
491
6.77k
                P->left = PJ_IO_UNITS_RADIANS;
492
11.3k
            if (strcmp(normalized_name, "Degree") == 0)
493
4.09k
                P->left = PJ_IO_UNITS_DEGREES;
494
11.3k
        }
495
11.4k
    }
496
497
11.8k
    if ((name = pj_param(P->ctx, P->params, "sxy_out").s) != nullptr) {
498
11.4k
        const char *normalized_name = nullptr;
499
11.4k
        f = get_unit_conversion_factor(name, &xy_out_is_linear,
500
11.4k
                                       &normalized_name);
501
11.4k
        if (f != 0.0) {
502
11.3k
            proj_log_trace(P, "xy_out unit: %s", normalized_name);
503
11.3k
        } else {
504
99
            f = pj_param(P->ctx, P->params, "dxy_out").f;
505
99
            if (f == 0.0 || 1.0 / f == 0.0) {
506
88
                proj_log_error(P, _("unknown xy_out unit"));
507
88
                return pj_default_destructor(
508
88
                    P, PROJ_ERR_INVALID_OP_ILLEGAL_ARG_VALUE);
509
88
            }
510
99
        }
511
11.3k
        Q->xy_factor /= f;
512
11.3k
        if (normalized_name != nullptr) {
513
11.3k
            if (strcmp(normalized_name, "Radian") == 0)
514
4.11k
                P->right = PJ_IO_UNITS_RADIANS;
515
11.3k
            if (strcmp(normalized_name, "Degree") == 0)
516
6.69k
                P->right = PJ_IO_UNITS_DEGREES;
517
11.3k
        }
518
11.3k
    }
519
520
11.7k
    if (xy_in_is_linear >= 0 && xy_out_is_linear >= 0 &&
521
11.2k
        xy_in_is_linear != xy_out_is_linear) {
522
0
        proj_log_error(P, _("inconsistent unit type between xy_in and xy_out"));
523
0
        return pj_default_destructor(P, PROJ_ERR_INVALID_OP_ILLEGAL_ARG_VALUE);
524
0
    }
525
526
11.7k
    if ((name = pj_param(P->ctx, P->params, "sz_in").s) != nullptr) {
527
1.08k
        const char *normalized_name = nullptr;
528
1.08k
        f = get_unit_conversion_factor(name, &z_in_is_linear, &normalized_name);
529
1.08k
        if (f != 0.0) {
530
955
            proj_log_trace(P, "z_in unit: %s", normalized_name);
531
955
        } else {
532
131
            f = pj_param(P->ctx, P->params, "dz_in").f;
533
131
            if (f == 0.0 || 1.0 / f == 0.0) {
534
14
                proj_log_error(P, _("unknown z_in unit"));
535
14
                return pj_default_destructor(
536
14
                    P, PROJ_ERR_INVALID_OP_ILLEGAL_ARG_VALUE);
537
14
            }
538
131
        }
539
1.07k
        Q->z_factor = f;
540
1.07k
    }
541
542
11.7k
    if ((name = pj_param(P->ctx, P->params, "sz_out").s) != nullptr) {
543
1.06k
        const char *normalized_name = nullptr;
544
1.06k
        f = get_unit_conversion_factor(name, &z_out_is_linear,
545
1.06k
                                       &normalized_name);
546
1.06k
        if (f != 0.0) {
547
986
            proj_log_trace(P, "z_out unit: %s", normalized_name);
548
986
        } else {
549
83
            f = pj_param(P->ctx, P->params, "dz_out").f;
550
83
            if (f == 0.0 || 1.0 / f == 0.0) {
551
15
                proj_log_error(P, _("unknown z_out unit"));
552
15
                return pj_default_destructor(
553
15
                    P, PROJ_ERR_INVALID_OP_ILLEGAL_ARG_VALUE);
554
15
            }
555
83
        }
556
1.05k
        Q->z_factor /= f;
557
1.05k
    }
558
559
11.7k
    if (z_in_is_linear >= 0 && z_out_is_linear >= 0 &&
560
877
        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
11.7k
    if ((name = pj_param(P->ctx, P->params, "st_in").s) != nullptr) {
566
17
        for (i = 0; (s = time_units[i].id) && strcmp(name, s); ++i)
567
13
            ;
568
569
4
        if (!s) {
570
1
            proj_log_error(P, _("unknown t_in unit"));
571
1
            return pj_default_destructor(P,
572
1
                                         PROJ_ERR_INVALID_OP_ILLEGAL_ARG_VALUE);
573
1
        }
574
575
3
        Q->t_in_id = i;
576
3
        proj_log_trace(P, "t_in unit: %s", time_units[i].name);
577
3
    }
578
579
11.7k
    s = nullptr;
580
11.7k
    if ((name = pj_param(P->ctx, P->params, "st_out").s) != nullptr) {
581
69
        for (i = 0; (s = time_units[i].id) && strcmp(name, s); ++i)
582
52
            ;
583
584
17
        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
13
        Q->t_out_id = i;
591
13
        proj_log_trace(P, "t_out unit: %s", time_units[i].name);
592
13
    }
593
594
11.7k
    return P;
595
11.7k
}