Coverage Report

Created: 2025-07-23 06:58

/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
1.76M
static PJ_XY forward_2d(PJ_LP lp, PJ *P) {
280
    /************************************************************************
281
        Forward unit conversions in the plane
282
    ************************************************************************/
283
1.76M
    struct pj_opaque_unitconvert *Q = (struct pj_opaque_unitconvert *)P->opaque;
284
1.76M
    PJ_COORD point = {{0, 0, 0, 0}};
285
1.76M
    point.lp = lp;
286
287
1.76M
    point.xy.x *= Q->xy_factor;
288
1.76M
    point.xy.y *= Q->xy_factor;
289
290
1.76M
    return point.xy;
291
1.76M
}
292
293
/***********************************************************************/
294
336
static PJ_LP reverse_2d(PJ_XY xy, PJ *P) {
295
    /************************************************************************
296
        Reverse unit conversions in the plane
297
    ************************************************************************/
298
336
    struct pj_opaque_unitconvert *Q = (struct pj_opaque_unitconvert *)P->opaque;
299
336
    PJ_COORD point = {{0, 0, 0, 0}};
300
336
    point.xy = xy;
301
302
336
    point.xy.x /= Q->xy_factor;
303
336
    point.xy.y /= Q->xy_factor;
304
305
336
    return point.lp;
306
336
}
307
308
/***********************************************************************/
309
1.76M
static PJ_XYZ forward_3d(PJ_LPZ lpz, PJ *P) {
310
    /************************************************************************
311
        Forward unit conversions the vertical component
312
    ************************************************************************/
313
1.76M
    struct pj_opaque_unitconvert *Q = (struct pj_opaque_unitconvert *)P->opaque;
314
1.76M
    PJ_COORD point = {{0, 0, 0, 0}};
315
1.76M
    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.76M
    const auto xy = forward_2d(point.lp, P);
323
1.76M
    point.xy = xy;
324
325
1.76M
    point.xyz.z *= Q->z_factor;
326
327
1.76M
    return point.xyz;
328
1.76M
}
329
330
/***********************************************************************/
331
336
static PJ_LPZ reverse_3d(PJ_XYZ xyz, PJ *P) {
332
    /************************************************************************
333
        Reverse unit conversions the vertical component
334
    ************************************************************************/
335
336
    struct pj_opaque_unitconvert *Q = (struct pj_opaque_unitconvert *)P->opaque;
336
336
    PJ_COORD point = {{0, 0, 0, 0}};
337
336
    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
336
    const auto lp = reverse_2d(point.xy, P);
345
336
    point.lp = lp;
346
347
336
    point.xyz.z /= Q->z_factor;
348
349
336
    return point.lpz;
350
336
}
351
352
/***********************************************************************/
353
1.76M
static void forward_4d(PJ_COORD &coo, PJ *P) {
354
    /************************************************************************
355
        Forward conversion of time units
356
    ************************************************************************/
357
1.76M
    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.76M
    const auto xyz = forward_3d(coo.lpz, P);
365
1.76M
    coo.xyz = xyz;
366
367
1.76M
    if (Q->t_in_id >= 0)
368
0
        coo.xyzt.t = time_units[Q->t_in_id].t_in(coo.xyzt.t);
369
1.76M
    if (Q->t_out_id >= 0)
370
0
        coo.xyzt.t = time_units[Q->t_out_id].t_out(coo.xyzt.t);
371
1.76M
}
372
373
/***********************************************************************/
374
336
static void reverse_4d(PJ_COORD &coo, PJ *P) {
375
    /************************************************************************
376
        Reverse conversion of time units
377
    ************************************************************************/
378
336
    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
336
    const auto lpz = reverse_3d(coo.xyz, P);
386
336
    coo.lpz = lpz;
387
388
336
    if (Q->t_out_id >= 0)
389
0
        coo.xyzt.t = time_units[Q->t_out_id].t_in(coo.xyzt.t);
390
336
    if (Q->t_in_id >= 0)
391
0
        coo.xyzt.t = time_units[Q->t_in_id].t_out(coo.xyzt.t);
392
336
}
393
394
/***********************************************************************/
395
static double get_unit_conversion_factor(const char *name, int *p_is_linear,
396
35.5k
                                         const char **p_normalized_name) {
397
    /***********************************************************************/
398
35.5k
    int i;
399
35.5k
    const char *s;
400
35.5k
    const PJ_UNITS *units = pj_list_linear_units();
401
402
    /* Try first with linear units */
403
708k
    for (i = 0; (s = units[i].id); ++i) {
404
676k
        if (strcmp(s, name) == 0) {
405
3.90k
            if (p_normalized_name) {
406
3.90k
                *p_normalized_name = units[i].name;
407
3.90k
            }
408
3.90k
            if (p_is_linear) {
409
3.90k
                *p_is_linear = 1;
410
3.90k
            }
411
3.90k
            return units[i].factor;
412
3.90k
        }
413
676k
    }
414
415
    /* And then angular units */
416
31.6k
    units = pj_list_angular_units();
417
48.6k
    for (i = 0; (s = units[i].id); ++i) {
418
48.1k
        if (strcmp(s, name) == 0) {
419
31.1k
            if (p_normalized_name) {
420
31.1k
                *p_normalized_name = units[i].name;
421
31.1k
            }
422
31.1k
            if (p_is_linear) {
423
31.1k
                *p_is_linear = 0;
424
31.1k
            }
425
31.1k
            return units[i].factor;
426
31.1k
        }
427
48.1k
    }
428
490
    if (p_normalized_name) {
429
490
        *p_normalized_name = nullptr;
430
490
    }
431
490
    if (p_is_linear) {
432
490
        *p_is_linear = -1;
433
490
    }
434
490
    return 0.0;
435
31.6k
}
436
437
/***********************************************************************/
438
16.9k
PJ *PJ_CONVERSION(unitconvert, 0) {
439
    /***********************************************************************/
440
16.9k
    struct pj_opaque_unitconvert *Q =
441
16.9k
        static_cast<struct pj_opaque_unitconvert *>(
442
16.9k
            calloc(1, sizeof(struct pj_opaque_unitconvert)));
443
16.9k
    const char *s, *name;
444
16.9k
    int i;
445
16.9k
    double f;
446
16.9k
    int xy_in_is_linear = -1;  /* unknown */
447
16.9k
    int xy_out_is_linear = -1; /* unknown */
448
16.9k
    int z_in_is_linear = -1;   /* unknown */
449
16.9k
    int z_out_is_linear = -1;  /* unknown */
450
451
16.9k
    if (nullptr == Q)
452
0
        return pj_default_destructor(P, PROJ_ERR_OTHER /*ENOMEM*/);
453
16.9k
    P->opaque = (void *)Q;
454
455
16.9k
    P->fwd4d = forward_4d;
456
16.9k
    P->inv4d = reverse_4d;
457
16.9k
    P->fwd3d = forward_3d;
458
16.9k
    P->inv3d = reverse_3d;
459
16.9k
    P->fwd = forward_2d;
460
16.9k
    P->inv = reverse_2d;
461
462
16.9k
    P->left = PJ_IO_UNITS_WHATEVER;
463
16.9k
    P->right = PJ_IO_UNITS_WHATEVER;
464
16.9k
    P->skip_fwd_prepare = 1;
465
16.9k
    P->skip_inv_prepare = 1;
466
467
    /* if no time input/output unit is specified we can skip them */
468
16.9k
    Q->t_in_id = -1;
469
16.9k
    Q->t_out_id = -1;
470
471
16.9k
    Q->xy_factor = 1.0;
472
16.9k
    Q->z_factor = 1.0;
473
474
16.9k
    if ((name = pj_param(P->ctx, P->params, "sxy_in").s) != nullptr) {
475
16.3k
        const char *normalized_name = nullptr;
476
16.3k
        f = get_unit_conversion_factor(name, &xy_in_is_linear,
477
16.3k
                                       &normalized_name);
478
16.3k
        if (f != 0.0) {
479
16.2k
            proj_log_trace(P, "xy_in unit: %s", normalized_name);
480
16.2k
        } else {
481
143
            f = pj_param(P->ctx, P->params, "dxy_in").f;
482
143
            if (f == 0.0 || 1.0 / f == 0.0) {
483
46
                proj_log_error(P, _("unknown xy_in unit"));
484
46
                return pj_default_destructor(
485
46
                    P, PROJ_ERR_INVALID_OP_ILLEGAL_ARG_VALUE);
486
46
            }
487
143
        }
488
16.3k
        Q->xy_factor = f;
489
16.3k
        if (normalized_name != nullptr) {
490
16.2k
            if (strcmp(normalized_name, "Radian") == 0)
491
8.45k
                P->left = PJ_IO_UNITS_RADIANS;
492
16.2k
            if (strcmp(normalized_name, "Degree") == 0)
493
7.18k
                P->left = PJ_IO_UNITS_DEGREES;
494
16.2k
        }
495
16.3k
    }
496
497
16.8k
    if ((name = pj_param(P->ctx, P->params, "sxy_out").s) != nullptr) {
498
16.2k
        const char *normalized_name = nullptr;
499
16.2k
        f = get_unit_conversion_factor(name, &xy_out_is_linear,
500
16.2k
                                       &normalized_name);
501
16.2k
        if (f != 0.0) {
502
16.1k
            proj_log_trace(P, "xy_out unit: %s", normalized_name);
503
16.1k
        } else {
504
138
            f = pj_param(P->ctx, P->params, "dxy_out").f;
505
138
            if (f == 0.0 || 1.0 / f == 0.0) {
506
124
                proj_log_error(P, _("unknown xy_out unit"));
507
124
                return pj_default_destructor(
508
124
                    P, PROJ_ERR_INVALID_OP_ILLEGAL_ARG_VALUE);
509
124
            }
510
138
        }
511
16.1k
        Q->xy_factor /= f;
512
16.1k
        if (normalized_name != nullptr) {
513
16.1k
            if (strcmp(normalized_name, "Radian") == 0)
514
7.20k
                P->right = PJ_IO_UNITS_RADIANS;
515
16.1k
            if (strcmp(normalized_name, "Degree") == 0)
516
8.33k
                P->right = PJ_IO_UNITS_DEGREES;
517
16.1k
        }
518
16.1k
    }
519
520
16.7k
    if (xy_in_is_linear >= 0 && xy_out_is_linear >= 0 &&
521
16.7k
        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
16.7k
    if ((name = pj_param(P->ctx, P->params, "sz_in").s) != nullptr) {
527
1.48k
        const char *normalized_name = nullptr;
528
1.48k
        f = get_unit_conversion_factor(name, &z_in_is_linear, &normalized_name);
529
1.48k
        if (f != 0.0) {
530
1.37k
            proj_log_trace(P, "z_in unit: %s", normalized_name);
531
1.37k
        } else {
532
105
            f = pj_param(P->ctx, P->params, "dz_in").f;
533
105
            if (f == 0.0 || 1.0 / f == 0.0) {
534
12
                proj_log_error(P, _("unknown z_in unit"));
535
12
                return pj_default_destructor(
536
12
                    P, PROJ_ERR_INVALID_OP_ILLEGAL_ARG_VALUE);
537
12
            }
538
105
        }
539
1.46k
        Q->z_factor = f;
540
1.46k
    }
541
542
16.7k
    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.37k
            proj_log_trace(P, "z_out unit: %s", normalized_name);
548
1.37k
        } else {
549
104
            f = pj_param(P->ctx, P->params, "dz_out").f;
550
104
            if (f == 0.0 || 1.0 / f == 0.0) {
551
59
                proj_log_error(P, _("unknown z_out unit"));
552
59
                return pj_default_destructor(
553
59
                    P, PROJ_ERR_INVALID_OP_ILLEGAL_ARG_VALUE);
554
59
            }
555
104
        }
556
1.41k
        Q->z_factor /= f;
557
1.41k
    }
558
559
16.6k
    if (z_in_is_linear >= 0 && z_out_is_linear >= 0 &&
560
16.6k
        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
16.6k
    if ((name = pj_param(P->ctx, P->params, "st_in").s) != nullptr) {
566
30
        for (i = 0; (s = time_units[i].id) && strcmp(name, s); ++i)
567
23
            ;
568
569
7
        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
5
        Q->t_in_id = i;
576
5
        proj_log_trace(P, "t_in unit: %s", time_units[i].name);
577
5
    }
578
579
16.6k
    s = nullptr;
580
16.6k
    if ((name = pj_param(P->ctx, P->params, "st_out").s) != nullptr) {
581
89
        for (i = 0; (s = time_units[i].id) && strcmp(name, s); ++i)
582
62
            ;
583
584
27
        if (!s) {
585
2
            proj_log_error(P, _("unknown t_out unit"));
586
2
            return pj_default_destructor(P,
587
2
                                         PROJ_ERR_INVALID_OP_ILLEGAL_ARG_VALUE);
588
2
        }
589
590
25
        Q->t_out_id = i;
591
25
        proj_log_trace(P, "t_out unit: %s", time_units[i].name);
592
25
    }
593
594
16.6k
    return P;
595
16.6k
}