001    /**
002     * The contents of this file are subject to the Mozilla Public License Version 1.1
003     * (the "License"); you may not use this file except in compliance with the License.
004     * You may obtain a copy of the License at http://www.mozilla.org/MPL/
005     * Software distributed under the License is distributed on an "AS IS" basis,
006     * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the
007     * specific language governing rights and limitations under the License.
008     *
009     * The Original Code is "CommmonTM.java".  Description:
010     * "Note: The class description below has been excerpted from the Hl7 2.4 documentation"
011     *
012     * The Initial Developer of the Original Code is University Health Network. Copyright (C)
013     * 2001.  All Rights Reserved.
014     *
015     * Contributor(s): ______________________________________.
016     *
017     * Alternatively, the contents of this file may be used under the terms of the
018     * GNU General Public License (the  ?GPL?), in which case the provisions of the GPL are
019     * applicable instead of those above.  If you wish to allow use of your version of this
020     * file only under the terms of the GPL and not to allow others to use your version
021     * of this file under the MPL, indicate your decision by deleting  the provisions above
022     * and replace  them with the notice and other provisions required by the GPL License.
023     * If you do not delete the provisions above, a recipient may use your version of
024     * this file under either the MPL or the GPL.
025     *
026     */
027    
028    package ca.uhn.hl7v2.model.primitive;
029    
030    import java.util.GregorianCalendar;
031    
032    import ca.uhn.hl7v2.model.DataTypeException;
033    import ca.uhn.hl7v2.model.DataTypeUtil;
034    import ca.uhn.log.HapiLog;
035    import ca.uhn.log.HapiLogFactory;
036    
037    /**
038     * This class contains functionality used by the TM class
039     * in the version 2.3.0, 2.3.1, and 2.4 packages
040     *
041     * Note: The class description below has been excerpted from the Hl7 2.4 documentation. Sectional
042     * references made below also refer to the same documentation.
043     *
044     * Format: HH[MM[SS[.S[S[S[S]]]]]][+/-ZZZZ]
045     * In prior versions of HL7, this data type was always specified to be in the
046     * format HHMM[SS[.SSSS]][+/-ZZZZ] using a 24 hour clock notation. In the
047     * current and future versions, the precision of a time may be expressed by
048     * limiting the number of digits used with the format specification as shown
049     * above. By site-specific agreement, HHMM[SS[.SSSS]][+/-ZZZZ] may be used where
050     * backward compatibility must be maintained.
051     * Thus, HH is used to specify a precision of "hour," HHMM is used to specify a
052     * precision of "minute," HHMMSS is used to specify a precision of seconds, and
053     * HHMMSS.SSSS is used to specify a precision of ten-thousandths of a second.
054     * In each of these cases, the time zone is an optional component. The fractional
055     * seconds could be sent by a transmitter who requires greater precision than whole
056     * seconds. Fractional representations of minutes, hours or other higher-order units
057     * of time are not permitted.
058     * Note: The time zone [+/-ZZZZ], when used, is restricted to legally-defined time zones
059     * and is represented in HHMM format.
060     * The time zone of the sender may be sent optionally as an offset from the coordinated
061     * universal time (previously known as Greenwich Mean Time). Where the time zone
062     * is not present in a particular TM field but is included as part of the date/time
063     * field in the MSH segment, the MSH value will be used as the default time zone.
064     * Otherwise, the time is understood to refer to the local time of the sender.
065     * Midnight is represented as 0000.
066     * Examples:|235959+1100| 1 second before midnight in a time zone eleven hours
067     * ahead of Universal Coordinated Time (i.e., east of Greenwich).
068     * |0800| Eight AM, local time of the sender.
069     * |093544.2312| 44.2312 seconds after Nine thirty-five AM, local time of sender.
070     * |13| 1pm (with a precision of hours), local time of sender.
071     * @author Neal Acharya
072     */
073    public class CommonTM {
074    
075        private static final HapiLog log = HapiLogFactory.getHapiLog(CommonTM.class);
076    
077        private String value;
078        private int hour;
079        private int minute;
080        private int second;
081        private float fractionOfSec;
082        private int offSet;
083        private char omitOffsetFg = 'n';
084    
085        /**
086         * Constructs a TM datatype with fields initialzed to zero and the value set to
087         * null.
088         */
089        public CommonTM() {
090            //initialize all DT fields
091            value = null;
092            hour = 0;
093            minute = 0;
094            second = 0;
095            fractionOfSec = 0;
096            offSet = -99;
097        } //end constructor
098    
099        /**
100         * Constructs a TM object with the given value.
101         * The stored value will be in the following
102         * format HH[MM[SS[.S[S[S[S]]]]]][+/-ZZZZ].
103         */
104        public CommonTM(String val) throws DataTypeException {
105            this.setValue(val);
106        } //end constructor
107    
108        /**
109         * This method takes in a string HL7 Time value and performs validations
110         * then sets the value field.  The stored value will be in the following
111         * format HH[MM[SS[.S[S[S[S]]]]]][+/-ZZZZ].
112         * Note: Trailing zeros supplied in the time value (HH[MM[SS[.S[S[S[S]]]]]])
113         * and GMT offset ([+/-ZZZZ]) will be preserved.
114         * Note: If the GMT offset is not supplied then the local
115         * time zone (using standard time zone format which is not modified for daylight savings)
116         * will be stored as a default.
117         */
118        public void setValue(String val) throws DataTypeException {
119    
120            if (val != null && !val.equals("") && !val.equals("\"\"")) {
121                //check to see if any of the following characters exist: "." or "+/-"
122                //this will help us determine the acceptable lengths
123    
124                int d = val.indexOf(".");
125                int sp = val.indexOf("+");
126                int sm = val.indexOf("-");
127                int indexOfSign = -1;
128                boolean offsetExists = false;
129                if ((sp != -1) || (sm != -1))
130                    offsetExists = true;
131                if (sp != -1)
132                    indexOfSign = sp;
133                if (sm != -1)
134                    indexOfSign = sm;
135    
136                try {
137                    //If the GMT offset exists then extract it from the input string and store it
138                    //in another variable called tempOffset. Also, store the time value
139                    //(without the offset)in a separate variable called timeVal.
140                    //If there is no GMT offset then simply set timeVal to val.
141                    String timeVal = val;
142                    String tempOffset = null;
143                    if (offsetExists) {
144                        timeVal = val.substring(0, indexOfSign);
145                        tempOffset = val.substring(indexOfSign);
146                    } //end if
147    
148                    if (offsetExists && (tempOffset.length() != 5)) {
149                        //The length of the GMT offset must be 5 characters (including the sign)
150                        String msg =
151                            "The length of the TM datatype value does not conform to an allowable"
152                                + " format. Format should conform to HH[MM[SS[.S[S[S[S]]]]]][+/-ZZZZ]";
153                        DataTypeException e = new DataTypeException(msg);
154                        throw e;
155                    } //end if
156    
157                    if (d != -1) {
158                        //here we know that decimal exists
159                        //thus length of the time value can be between 8 and 11 characters
160                        if ((timeVal.length() < 8) || (timeVal.length() > 11)) {
161                            String msg =
162                                "The length of the TM datatype value does not conform to an allowable"
163                                    + " format. Format should conform to HH[MM[SS[.S[S[S[S]]]]]][+/-ZZZZ]";
164                            DataTypeException e = new DataTypeException(msg);
165                            throw e;
166                        } //end if
167                    } //end if
168    
169                    if (d == -1) {
170                        //here we know that the decimal does not exist
171                        //thus length of the time value can be 2 or 4 or 6 characters
172                        if ((timeVal.length() != 2) && (timeVal.length() != 4) && (timeVal.length() != 6)) {
173                            String msg =
174                                "The length of the TM datatype value does not conform to an allowable"
175                                    + " format. Format should conform to HH[MM[SS[.S[S[S[S]]]]]][+/-ZZZZ]";
176                            DataTypeException e = new DataTypeException(msg);
177                            throw e;
178                        } //end if
179                    } //end if
180    
181                    //We will now try to validate the timeVal portion of the TM datatype value
182                    if (timeVal.length() >= 2) {
183                        //extract the hour data from the input value.  If the first 2 characters
184                        //are not numeric then a number format exception will be generated
185                        int hrInt = Integer.parseInt(timeVal.substring(0, 2));
186                        //check to see if the hour value is valid
187                        if ((hrInt < 0) || (hrInt > 23)) {
188                            String msg = "The hour value of the TM datatype must be >=0 and <=23";
189                            DataTypeException e = new DataTypeException(msg);
190                            throw e;
191                        } //end if
192                        hour = hrInt;
193                    } //end if
194    
195                    if (timeVal.length() >= 4) {
196                        //extract the minute data from the input value
197                        //If these characters are not numeric then a number
198                        //format exception will be generated
199                        int minInt = Integer.parseInt(timeVal.substring(2, 4));
200                        //check to see if the minute value is valid
201                        if ((minInt < 0) || (minInt > 59)) {
202                            String msg = "The minute value of the TM datatype must be >=0 and <=59";
203                            DataTypeException e = new DataTypeException(msg);
204                            throw e;
205                        } //end if
206                        minute = minInt;
207                    } //end if
208    
209                    if (timeVal.length() >= 6) {
210                        //extract the seconds data from the input value
211                        //If these characters are not numeric then a number
212                        //format exception will be generated
213                        int secInt = Integer.parseInt(timeVal.substring(4, 6));
214                        //check to see if the seconds value is valid
215                        if ((secInt < 0) || (secInt > 59)) {
216                            String msg = "The seconds value of the TM datatype must be >=0 and <=59";
217                            DataTypeException e = new DataTypeException(msg);
218                            throw e;
219                        } //end if
220                        second = secInt;
221                    } //end if
222    
223                    if (timeVal.length() >= 8) {
224                        //extract the fractional second value from the input value
225                        //If these characters are not numeric then a number
226                        //format exception will be generated
227                        float fract = Float.parseFloat(timeVal.substring(6));
228                        //check to see if the fractional second value is valid
229                        if ((fract < 0) || (fract >= 1)) {
230                            String msg = "The fractional second value of the TM datatype must be >= 0 and < 1";
231                            DataTypeException e = new DataTypeException(msg);
232                            throw e;
233                        } //end if
234                        fractionOfSec = fract;
235                    } //end if
236    
237                    //We will now try to validate the tempOffset portion of the TM datatype value
238                    if (offsetExists) {
239                        //in case the offset are a series of zeros we should not omit displaying
240                        //it in the return value from the getValue() method
241                        omitOffsetFg = 'n';
242                        //remove the sign from the temp offset
243                        String tempOffsetNoS = tempOffset.substring(1);
244                        //extract the hour data from the offset value.  If the first 2 characters
245                        //are not numeric then a number format exception will be generated
246                        int offsetInt = Integer.parseInt(tempOffsetNoS.substring(0, 2));
247                        //check to see if the hour value is valid
248                        if ((offsetInt < 0) || (offsetInt > 23)) {
249                            String msg = "The GMT offset hour value of the TM datatype must be >=0 and <=23";
250                            DataTypeException e = new DataTypeException(msg);
251                            throw e;
252                        } //end if
253                        //extract the minute data from the offset value.  If these characters
254                        //are not numeric then a number format exception will be generated
255                        offsetInt = Integer.parseInt(tempOffsetNoS.substring(2, 4));
256                        //check to see if the minute value is valid
257                        if ((offsetInt < 0) || (offsetInt > 59)) {
258                            String msg = "The GMT offset minute value of the TM datatype must be >=0 and <=59";
259                            DataTypeException e = new DataTypeException(msg);
260                            throw e;
261                        } //end if
262                        //validation done, update the offSet field
263                        offSet = Integer.parseInt(tempOffsetNoS);
264                        //add the sign back to the offset if it is negative
265                        if (sm != -1) {
266                            offSet = -1 * offSet;
267                        } //end if
268                    } //end if
269    
270                    //If the GMT offset has not been supplied then set the offset to the
271                    //local timezone
272                    //[Bryan: changing this to omit time zone because erroneous if parser in different zone than sender]
273                    if (!offsetExists) {
274                        omitOffsetFg = 'y';
275                        // set the offSet field to the current time and local time zone
276                        //offSet = DataTypeUtil.getLocalGMTOffset();
277                    } //end if
278    
279                    //validations are now done store the time value into the private value field
280                    value = timeVal;
281                } //end try
282    
283                catch (DataTypeException e) {
284                    throw e;
285                } //end catch
286    
287                catch (Exception e) {
288                    throw new DataTypeException(e);
289                } //end catch
290            } //end if
291            else {
292                //set the private value field to null or empty space.
293                value = val;
294            } //end else
295        } //end method
296    
297        /**
298         * This method takes in an integer value for the hour and performs validations,
299         * it then sets the value field formatted as an HL7 time
300         * value with hour precision (HH).
301         */
302        public void setHourPrecision(int hr) throws DataTypeException {
303            try {
304                //validate input value
305                if ((hr < 0) || (hr > 23)) {
306                    String msg = "The hour value of the TM datatype must be >=0 and <=23";
307                    DataTypeException e = new DataTypeException(msg);
308                    throw e;
309                } //end if
310                hour = hr;
311                minute = 0;
312                second = 0;
313                fractionOfSec = 0;
314                offSet = 0;
315                //Here the offset is not defined, we should omit showing it in the
316                //return value from the getValue() method
317                omitOffsetFg = 'y';
318                value = DataTypeUtil.preAppendZeroes(hr, 2);
319            } //end try
320    
321            catch (DataTypeException e) {
322                throw e;
323            } //end catch
324    
325            catch (Exception e) {
326                throw new DataTypeException(e.getMessage());
327            } //end catch
328    
329        } //end method
330    
331        /**
332         * This method takes in integer values for the hour and minute and performs validations,
333         * it then sets the value field formatted as an HL7 time value
334         * with hour&minute precision (HHMM).
335         */
336        public void setHourMinutePrecision(int hr, int min) throws DataTypeException {
337            try {
338                this.setHourPrecision(hr);
339                //validate input minute value
340                if ((min < 0) || (min > 59)) {
341                    String msg = "The minute value of the TM datatype must be >=0 and <=59";
342                    DataTypeException e = new DataTypeException(msg);
343                    throw e;
344                } //end if
345                minute = min;
346                second = 0;
347                fractionOfSec = 0;
348                offSet = 0;
349                //Here the offset is not defined, we should omit showing it in the
350                //return value from the getValue() method
351                omitOffsetFg = 'y';
352                value = value + DataTypeUtil.preAppendZeroes(min, 2);
353            } //end try
354    
355            catch (DataTypeException e) {
356                throw e;
357            } //end catch
358    
359            catch (Exception e) {
360                throw new DataTypeException(e.getMessage());
361            } //end catch
362        } //end method
363    
364        /**
365         * This method takes in integer values for the hour, minute, seconds, and fractional seconds
366         * (going to the tenthousandths precision).
367         * The method performs validations and then sets the value field formatted as an
368         * HL7 time value with a precision that starts from the hour and goes down to the tenthousandths
369         * of a second (HHMMSS.SSSS).
370         * Note: all of the precisions from tenths down to tenthousandths of a
371         * second are optional. If the precision goes below tenthousandths of a second then the second
372         * value will be rounded to the nearest tenthousandths of a second.
373         */
374        public void setHourMinSecondPrecision(int hr, int min, float sec) throws DataTypeException {
375            try {
376                this.setHourMinutePrecision(hr, min);
377                //multiply the seconds input value by 10000 and round the result
378                //then divide the number by tenthousand and store it back.
379                //This will round the fractional seconds to the nearest tenthousandths
380                int secMultRound = Math.round(10000F * sec);
381                sec = secMultRound / 10000F;
382                //Now store the second and fractional component
383                second = (int) Math.floor(sec);
384                            //validate input seconds value
385                            if ((second < 0) || (second >= 60)) {
386                                    String msg = "The (rounded) second value of the TM datatype must be >=0 and <60";
387                                    DataTypeException e = new DataTypeException(msg);
388                                    throw e;
389                            } //end if
390                int fractionOfSecInt = (int) (secMultRound - (second * 10000));
391                fractionOfSec = fractionOfSecInt / 10000F;
392                String fractString = "";
393                //Now convert the fractionOfSec field to a string without the leading zero
394                if (fractionOfSec != 0.0F) {
395                    fractString = (Float.toString(fractionOfSec)).substring(1);
396                } //end if
397                //Now update the value field
398                offSet = 0;
399                //Here the offset is not defined, we should omit showing it in the
400                //return value from the getValue() method
401                omitOffsetFg = 'y';
402                value = value + DataTypeUtil.preAppendZeroes(second, 2) + fractString;
403            } //end try
404    
405            catch (DataTypeException e) {
406                throw e;
407            } //end catch
408    
409            catch (Exception e) {
410                throw new DataTypeException(e);
411            } //end catch
412        } //end method
413    
414        /**
415         * This method takes in the four digit (signed) GMT offset and sets the offset
416         * field
417         */
418        public void setOffset(int signedOffset) throws DataTypeException {
419            try {
420                //When this function is called an offset is being created/updated
421                //we should not omit displaying it in the return value from
422                //the getValue() method
423                omitOffsetFg = 'n';
424                String offsetStr = Integer.toString(signedOffset);
425                if ((signedOffset >= 0 && offsetStr.length() > 4) || (signedOffset < 0 && offsetStr.length() > 5)) {
426                    //The length of the GMT offset must be no greater than 5 characters (including the sign)
427                    String msg =
428                        "The length of the GMT offset for the TM datatype value does"
429                            + " not conform to the allowable format [+/-ZZZZ]. Value: " + signedOffset;
430                    DataTypeException e = new DataTypeException(msg);
431                    throw e;
432                } //end if
433                //obtain the absolute value of the input
434                int absOffset = Math.abs(signedOffset);
435                //extract the hour data from the offset value.
436                //first preappend zeros so we have a 4 char offset value (without sign)
437                offsetStr = DataTypeUtil.preAppendZeroes(absOffset, 4);
438                int hrOffsetInt = Integer.parseInt(offsetStr.substring(0, 2));
439                //check to see if the hour value is valid
440                if ((hrOffsetInt < 0) || (hrOffsetInt > 23)) {
441                    String msg = "The GMT offset hour value of the TM datatype must be >=0 and <=23";
442                    DataTypeException e = new DataTypeException(msg);
443                    throw e;
444                } //end if
445                //extract the minute data from the offset value.
446                int minOffsetInt = Integer.parseInt(offsetStr.substring(2, 4));
447                //check to see if the minute value is valid
448                if ((minOffsetInt < 0) || (minOffsetInt > 59)) {
449                    String msg = "The GMT offset minute value of the TM datatype must be >=0 and <=59";
450                    DataTypeException e = new DataTypeException(msg);
451                    throw e;
452                } //end if
453                //The input value is valid, now store it in the offset field
454                offSet = signedOffset;
455            } //end try
456    
457            catch (DataTypeException e) {
458                throw e;
459            } //end catch
460    
461            catch (Exception e) {
462                throw new DataTypeException(e);
463            } //end catch
464        } //end method
465    
466        /**
467         * Returns the HL7 TM string value.
468         */
469        public String getValue() {
470            //combine the value field with the offSet field and return it
471            String returnVal = null;
472            if (value != null && !value.equals("")) {
473                if (omitOffsetFg == 'n' && !value.equals("\"\"")) {
474                    int absOffset = Math.abs(offSet);
475                    String sign = "";
476                    if (offSet >= 0) {
477                        sign = "+";
478                    } //end if
479                    else {
480                        sign = "-";
481                    } //end else
482                    returnVal = value + sign + DataTypeUtil.preAppendZeroes(absOffset, 4);
483                }
484                else {
485                    returnVal = value;
486                } //end else
487            } //end if
488            return returnVal;
489        } //end method
490    
491        /**
492         * Returns the hour as an integer.
493         */
494        public int getHour() {
495            return hour;
496        } //end method
497    
498        /**
499         * Returns the minute as an integer.
500         */
501        public int getMinute() {
502            return minute;
503        } //end method
504    
505        /**
506         * Returns the second as an integer.
507         */
508        public int getSecond() {
509            return second;
510        } //end method
511    
512        /**
513         * Returns the fractional second value as a float.
514         */
515        public float getFractSecond() {
516            return fractionOfSec;
517        } //end method
518    
519        /**
520         * Returns the GMT offset value as an integer, -99 if not set.  
521         */
522        public int getGMTOffset() {
523            return offSet;
524        } //end method
525    
526        /**
527         * Returns a string value representing the input Gregorian Calendar object in
528         * an Hl7 Time Format.
529         */
530        public static String toHl7TMFormat(GregorianCalendar cal) throws DataTypeException {
531            String val = "";
532            try {
533                //set the input cal object so that it can report errors
534                //on it's value
535                cal.setLenient(false);
536                int calHour = cal.get(GregorianCalendar.HOUR_OF_DAY);
537                int calMin = cal.get(GregorianCalendar.MINUTE);
538                int calSec = cal.get(GregorianCalendar.SECOND);
539                int calMilli = cal.get(GregorianCalendar.MILLISECOND);
540                //the inputs seconds and milli seconds should be combined into a float type
541                float fractSec = calMilli / 1000F;
542                float calSecFloat = calSec + fractSec;
543                int calOffset = cal.get(GregorianCalendar.ZONE_OFFSET) + cal.get(GregorianCalendar.DST_OFFSET); 
544                //Note the input's Offset value is in milliseconds, we must convert it to
545                //a 4 digit integer in the HL7 Offset format.
546                int offSetSignInt;
547                if (calOffset < 0) {
548                    offSetSignInt = -1;
549                }
550                else {
551                    offSetSignInt = 1;
552                }
553                //get the absolute value of the gmtOffSet
554                int absGmtOffSet = Math.abs(calOffset);
555                int gmtOffSetHours = absGmtOffSet / (3600 * 1000);
556                int gmtOffSetMin = (absGmtOffSet / 60000) % (60);
557                //reset calOffset
558                calOffset = ((gmtOffSetHours * 100) + gmtOffSetMin) * offSetSignInt;
559                //Create an object of the TS class and populate it with the above values
560                //then return the HL7 string value from the object
561                CommonTM tm = new CommonTM();
562                tm.setHourMinSecondPrecision(calHour, calMin, calSecFloat);
563                tm.setOffset(calOffset);
564                val = tm.getValue();
565            } // end try
566    
567            catch (DataTypeException e) {
568                throw e;
569            } //end catch
570    
571            catch (Exception e) {
572                throw new DataTypeException(e);
573            } //end catch
574            return val;
575        } //end method
576    
577    } //end class