Repository: obspy/obspy

Issue:
reading of seconds on Nordic Type 4 lines
obspy incorrectly reads pick times (specifically seconds) when the seconds are given to millisecond precision (I assume this happens when I use high-accuracy mode, but in any case it is a "standard" output for SEISAN as can be seen in the definition of Nordic files (screenshot attached).

I have attached an example Nordic file: the first and third "events" are in this high-accuracy notation.  I have also attached a script to read this file and print out the pick times so that you can compare for yourself(ves).  I had to add the suffix "txt" so that it could be attached.

The solution is to read in line[22:28] as seconds instead of line[23:28], in obspy.io.nordic.core, I have attached a screenshot of the relevant code from the current git in which the relevant lines are 745 and 749

I am using obspy version 1.1.1, installed using conda (and conda-forge)


![nordic_type4line](https://user-images.githubusercontent.com/3601379/53949060-90981180-40c9-11e9-97b2-ec3f562365a0.png)
[correct_10_11.txt](https://github.com/obspy/obspy/files/2940653/correct_10_11.txt)
[nordic_example.py.txt](https://github.com/obspy/obspy/files/2940666/nordic_example.py.txt)
![nordic_code_example](https://user-images.githubusercontent.com/3601379/53949728-f5a03700-40ca-11e9-9dc7-64cf5c1de41f.png)




Discussion:
@calum-chamberlain you did the nordic stuff, right? Can you have a look?
@WayneCrawford Apologies and thanks for pointing that out - do you want to make a pull request to fix this? Happy to do it for you if you don't want to.  It would be good to add a high-precision test file as well (I don't have one, probably hence this popping up). It looks like [line 749](https://github.com/obspy/obspy/blob/master/obspy/io/nordic/core.py#L749) has a typo as well (should be `line[23:28]`, or rather `line[22:28]` as you state).

On that note @WayneCrawford if you are an extensive seisan user - do you have any other interesting files to add, or have noticed anything else not working as it should in Nordic? Any contributions on the nordic front would be really welcome!
This sounds like a quick fix, so I'll add it to 1.2.0 which we will try to finalize on 15th, i.e. next Friday.
Thanks for doing this.  I saw the discrepancy on line 749 as well but, as I didn't know what specific case that handled, I wasn't sure it was a typo.  I'm not a very strong SEISAN user so I don't think I have any interesting files to contribute, but I will keep an eye out.  

The only other problem I was aware of with Nordic File reading was the existence of "seconds" >= 60, it seems your line 749 tries to handle this but I'm not sure it is fully complete or robust.  First, I've seen seconds > 100! (see attached example).  Then, it looks like pick_hour is being placed in the "minutes" slot.  I would replace with something like:

        seconds=float(line[22:28])
        if seconds<60. :
            time = UTCDateTime(evtime.year, evtime.month, evtime.day,
                               pick_hour, int(line[20:22]),
                               seconds) + day_add
        else:
             time = UTCDateTime(evtime.year, evtime.month, evtime.day,
                               pick_hour, int(line[20:22]),
                               seconds-60.) +60. + day_add

I don't even know if SEISAN limits itself to <120 seconds: if not, the second part would have to be a bit more clever...
 [nordic_crazyseconds.txt](https://github.com/obspy/obspy/files/2945603/nordic_crazyseconds.txt)
          
I'm still not very familiar with GitHub, pulls, and, above all, testing what I've done, so I'd prefer it if one of you could do so.  If not, I can give it a try!


> I don't even know if SEISAN limits itself to <120 seconds: if not, the second part would have to be a bit more clever...

We added a `strict` keyword to the `UTCDateTime` object for cases such as these in #2232. So as long as `strict` == `False` you can just pass as many seconds as you like to the `UTCDateTime` constructor like so:

```python
from obpsy import UTCDateTime

time = UTCDateTime(year=2001, month=1, day=1, hour=2, minute=23, second=14242, strict=False)

```
This will make the seisan code considerably simpler. 

@WayneCrawford all good, thanks for that info - I will make a PR this weekend.

Thanks for that pointer @d-chambers as well, that will make it much nicer! Wish I had spotted that.
Implemented a fix in #2350 - I didn't end up using the strict argument, it was easier to add the seconds as a float after instantiating the UTCDateTime object.  The string formatting is really ugly in nordic, I would like to clean that up at some point...

@WayneCrawford could you let me know whether seisan is happy reading *high-accuracy* phase times when not run in high-accuracy mode?  I just want to know if it is okay to, by default, write out high-accuracy phase times?

Resolving patch:
diff --git a/obspy/io/nordic/__init__.py b/obspy/io/nordic/__init__.py
--- a/obspy/io/nordic/__init__.py
+++ b/obspy/io/nordic/__init__.py
@@ -12,3 +12,11 @@
 from __future__ import (absolute_import, division, print_function,
                         unicode_literals)
 from future.builtins import *  # NOQA
+
+
+class NordicParsingError(Exception):
+    """
+    Internal general error for IO operations in obspy.core.io.nordic.
+    """
+    def __init__(self, value):
+        self.value = value
diff --git a/obspy/io/nordic/core.py b/obspy/io/nordic/core.py
--- a/obspy/io/nordic/core.py
+++ b/obspy/io/nordic/core.py
@@ -14,13 +14,18 @@
     the arrival.pick_id linking the arrival (which contain calculated
     information) with the pick.resource_id (where the pick contains only
     physical measured information).
+
+.. versionchanged:: 1.2.0
+
+    The number of stations used to calculate the origin was previously
+    incorrectly stored in a comment. From version 1.2.0 this is now stored
+    in `origin.quality.used_station_count`
 """
 from __future__ import (absolute_import, division, print_function,
                         unicode_literals)
 from future.builtins import *  # NOQA @UnusedWildImport
 
 import warnings
-from collections import defaultdict
 import datetime
 import os
 import io
@@ -28,27 +33,23 @@
 from obspy import UTCDateTime, read
 from obspy.geodetics import kilometers2degrees, degrees2kilometers
 from obspy.core.event import (
-    Event, Origin, Magnitude, Comment, Catalog, EventDescription, CreationInfo,
+    Event, Origin, Magnitude, Catalog, EventDescription, CreationInfo,
     OriginQuality, Pick, WaveformStreamID, Arrival, Amplitude,
     FocalMechanism, MomentTensor, NodalPlane, NodalPlanes, QuantityError,
     Tensor, ResourceIdentifier)
+from obspy.io.nordic import NordicParsingError
+from obspy.io.nordic.utils import (
+    _int_conv, _str_conv, _float_conv, _evmagtonor, _nortoevmag,
+    _get_line_tags)
 
 
-mag_mapping = {"ML": "L", "MLv": "L", "mB": "B", "Ms": "s", "MS": "S",
-               "MW": "W", "MbLg": "G", "Mc": "C"}
-
-onsets = {'I': 'impulsive', 'E': 'emergent'}
-# List of currently implemented line-endings, which in Nordic mark what format
-# info in that line will be.
-accepted_tags = ['1', '6', '7', 'E', ' ', 'F', 'M', '3']
-
-
-class NordicParsingError(Exception):
-    """
-    Internal general error for IO operations in obspy.core.io.nordic.
-    """
-    def __init__(self, value):
-        self.value = value
+POLARITY_MAPPING = {"": "undecidable", "C": "positive", "D": "negative"}
+INV_POLARITY_MAPPING = {item: key for key, item in POLARITY_MAPPING.items()}
+ONSET_MAPPING = {'I': 'impulsive', 'E': 'emergent'}
+INV_ONSET_MAPPING = {item: key for key, item in ONSET_MAPPING.items()}
+EVALUATION_MAPPING = {'A': 'automatic', ' ': 'manual'}
+INV_EVALUTATION_MAPPING = {
+    item: key for key, item in EVALUATION_MAPPING.items()}
 
 
 def _is_sfile(sfile, encoding='latin-1'):
@@ -98,155 +99,6 @@ def _is_sfile(sfile, encoding='latin-1'):
         return False
 
 
-def _get_line_tags(f, report=True):
-    """
-    Associate lines with a known line-type
-    :param f: File open in read
-    :param report: Whether to report warnings about lines not implemented
-    """
-    f.seek(0)
-    line = f.readline()
-    if len(line.rstrip()) != 80:
-        # Cannot be Nordic
-        raise NordicParsingError(
-            "Lines are not 80 characters long: not a nordic file")
-    f.seek(0)
-    tags = defaultdict(list)
-    for i, line in enumerate(f):
-        try:
-            line_id = line.rstrip()[79]
-        except IndexError:
-            line_id = ' '
-        if line_id in accepted_tags:
-            tags[line_id].append((line, i))
-        elif report:
-            warnings.warn("Lines of type %s have not been implemented yet, "
-                          "please submit a development request" % line_id)
-    return tags
-
-
-def _int_conv(string):
-    """
-    Convenience tool to convert from string to integer.
-
-    If empty string return None rather than an error.
-
-    >>> _int_conv('12')
-    12
-    >>> _int_conv('')
-
-    """
-    try:
-        intstring = int(string)
-    except Exception:
-        intstring = None
-    return intstring
-
-
-def _float_conv(string):
-    """
-    Convenience tool to convert from string to float.
-
-    If empty string return None rather than an error.
-
-    >>> _float_conv('12')
-    12.0
-    >>> _float_conv('')
-    >>> _float_conv('12.324')
-    12.324
-    """
-    try:
-        floatstring = float(string)
-    except Exception:
-        floatstring = None
-    return floatstring
-
-
-def _str_conv(number, rounded=False):
-    """
-    Convenience tool to convert a number, either float or int into a string.
-
-    If the int or float is None, returns empty string.
-
-    >>> print(_str_conv(12.3))
-    12.3
-    >>> print(_str_conv(12.34546, rounded=1))
-    12.3
-    >>> print(_str_conv(None))
-    <BLANKLINE>
-    >>> print(_str_conv(1123040))
-    11.2e5
-    """
-    if not number:
-        return str(' ')
-    if not rounded and isinstance(number, (float, int)):
-        if number < 100000:
-            string = str(number)
-        else:
-            exponant = int('{0:.2E}'.format(number).split('E+')[-1]) - 1
-            divisor = 10 ** exponant
-            string = '{0:.1f}'.format(number / divisor) + 'e' + str(exponant)
-    elif rounded == 2 and isinstance(number, (float, int)):
-        if number < 100000:
-            string = '{0:.2f}'.format(number)
-        else:
-            exponant = int('{0:.2E}'.format(number).split('E+')[-1]) - 1
-            divisor = 10 ** exponant
-            string = '{0:.2f}'.format(number / divisor) + 'e' + str(exponant)
-    elif rounded == 1 and isinstance(number, (float, int)):
-        if number < 100000:
-            string = '{0:.1f}'.format(number)
-        else:
-            exponant = int('{0:.2E}'.format(number).split('E+')[-1]) - 1
-            divisor = 10 ** exponant
-            string = '{0:.1f}'.format(number / divisor) + 'e' + str(exponant)
-    else:
-        return str(number)
-    return string
-
-
-def _evmagtonor(mag_type):
-    """
-    Switch from obspy event magnitude types to seisan syntax.
-
-    >>> print(_evmagtonor('mB'))  # doctest: +SKIP
-    B
-    >>> print(_evmagtonor('M'))  # doctest: +SKIP
-    W
-    >>> print(_evmagtonor('bob'))  # doctest: +SKIP
-    <BLANKLINE>
-    """
-    if mag_type == 'M':
-        warnings.warn('Converting generic magnitude to moment magnitude')
-        return "W"
-    try:
-        mag = mag_mapping[mag_type]
-    except KeyError:
-        warnings.warn(mag_type + ' is not convertible')
-        return ''
-    return mag
-
-
-def _nortoevmag(mag_type):
-    """
-    Switch from nordic type magnitude notation to obspy event magnitudes.
-
-    >>> print(_nortoevmag('B'))  # doctest: +SKIP
-    mB
-    >>> print(_nortoevmag('bob'))  # doctest: +SKIP
-    <BLANKLINE>
-    """
-    if mag_type.upper() == "L":
-        return "ML"
-    inv_mag_mapping = {item: key for key, item in mag_mapping.items()}
-    try:
-        mag = inv_mag_mapping[mag_type]
-    except KeyError:
-        warnings.warn(mag_type + ' is not convertible')
-        return ''
-    return mag
-
-
 def readheader(sfile, encoding='latin-1'):
     """
     Read header information from a seisan nordic format S-file.
@@ -349,21 +201,29 @@ def _read_origin(line):
         except ValueError:
             new_event.origins[0].__dict__[key] = None
     if new_event.origins[0].depth:
-        new_event.origins[0].depth *= 1000
+        new_event.origins[0].depth *= 1000.
     if line[43].strip():
         warnings.warn("Depth indicator {0} has not been mapped "
                       "to the event".format(line[43]))
     if line[44].strip():
         warnings.warn("Origin location indicator {0} has not been mapped "
                       "to the event".format(line[44]))
+    if line[10] == "F":
+        new_event.origins[0].time_fixed = True
     new_event.creation_info = CreationInfo(agency_id=line[45:48].strip())
     new_event.origins[0].creation_info = CreationInfo(
         agency_id=line[45:48].strip())
-    ksta = Comment(text='Number of stations=' + line[49:51].strip())
-    new_event.origins[0].comments.append(ksta)
-    if _float_conv(line[51:55]) is not None:
+    used_station_count = line[49:51].strip()
+    if used_station_count != '':
         new_event.origins[0].quality = OriginQuality(
-            standard_error=_float_conv(line[51:55]))
+            used_station_count=int(used_station_count))
+    timeres = _float_conv(line[51:55])
+    if timeres is not None:
+        if new_event.origins[0].quality is not None:
+            new_event.origins[0].quality.standard_error = timeres
+        else:
+            new_event.origins[0].quality = OriginQuality(
+                standard_error=timeres)
     # Read in magnitudes if they are there.
     magnitudes = []
     magnitudes.extend(_read_mags(line, new_event))
@@ -714,7 +574,11 @@ def _read_picks(tagged_lines, new_event):
         if len(line) < 80:
             line = line.ljust(80)  # Pick-lines without a tag may be short.
         weight = line[14]
-        if weight == '_':
+        if weight not in ' 012349_':  # Long phase name
+            weight = line[8]
+            phase = line[10:17].strip()
+            polarity = ''
+        elif weight == '_':
             phase = line[10:17]
             weight = 0
             polarity = ''
@@ -723,34 +587,23 @@ def _read_picks(tagged_lines, new_event):
             polarity = line[16]
             if weight == ' ':
                 weight = 0
-        polarity_maps = {"": "undecidable", "C": "positive", "D": "negative"}
-        try:
-            polarity = polarity_maps[polarity]
-        except KeyError:
-            polarity = "undecidable"
+        polarity = POLARITY_MAPPING.get(polarity, "undecidable")
         # It is valid nordic for the origin to be hour 23 and picks to be hour
         # 00 or 24: this signifies a pick over a day boundary.
-        if int(line[18:20]) == 0 and evtime.hour == 23:
+        pick_hour = int(line[18:20])
+        pick_minute = int(line[20:22])
+        pick_seconds = float(line[22:29])  # 29 should be blank, but sometimes
+        # SEISAN appears to overflow here, see #2348
+        if pick_hour == 0 and evtime.hour == 23:
             day_add = 86400
-            pick_hour = 0
-        elif int(line[18:20]) == 24:
+        elif pick_hour >= 24:  # Nordic supports up to 48 hours advanced.
             day_add = 86400
-            pick_hour = 0
+            pick_hour -= 24
         else:
             day_add = 0
-            pick_hour = int(line[18:20])
-        try:
-            time = UTCDateTime(evtime.year, evtime.month, evtime.day,
-                               pick_hour, int(line[20:22]),
-                               float(line[23:28])) + day_add
-        except ValueError:
-            time = UTCDateTime(evtime.year, evtime.month, evtime.day,
-                               int(line[18:20]), pick_hour,
-                               float("0." + line[23:38].split('.')[1])) +\
-                60 + day_add
-            # Add 60 seconds on to the time, this copes with s-file
-            # preference to write seconds in 1-60 rather than 0-59 which
-            # datetime objects accept
+        time = UTCDateTime(
+            year=evtime.year, month=evtime.month, day=evtime.day,
+            hour=pick_hour, minute=pick_minute) + (pick_seconds + day_add)
         if header[57:60] == 'AIN':
             ain = _float_conv(line[57:60])
             warnings.warn('AIN: %s in header, currently unsupported' % ain)
@@ -766,17 +619,14 @@ def _read_picks(tagged_lines, new_event):
         pick = Pick(waveform_id=_waveform_id, phase_hint=phase,
                     polarity=polarity, time=time)
         try:
-            pick.onset = onsets[line[9]]
+            pick.onset = ONSET_MAPPING[line[9]]
         except KeyError:
             pass
-        if line[15] == 'A':
-            pick.evaluation_mode = 'automatic'
-        else:
-            pick.evaluation_mode = 'manual'
+        pick.evaluation_mode = EVALUATION_MAPPING.get(line[15], "manual")
         # Note these two are not always filled - velocity conversion not yet
         # implemented, needs to be converted from km/s to s/deg
         # if not velocity == 999.0:
-            # new_event.picks[pick_index].horizontal_slowness = 1.0 / velocity
+        #     new_event.picks[pick_index].horizontal_slowness = 1.0 / velocity
         if _float_conv(line[46:51]) is not None:
             pick.backazimuth = _float_conv(line[46:51])
         # Create new obspy.event.Amplitude class which references above Pick
@@ -802,9 +652,9 @@ def _read_picks(tagged_lines, new_event):
             if snr:
                 _amplitude.snr = snr
             new_event.amplitudes.append(_amplitude)
-        elif _int_conv(line[28:33]) is not None:
-            # Create an amplitude instance for code duration also
-            _amplitude = Amplitude(generic_amplitude=_int_conv(line[28:33]),
+        elif _int_conv(line[29:33]) is not None:
+            # Create an amplitude instance for coda duration also
+            _amplitude = Amplitude(generic_amplitude=_int_conv(line[29:33]),
                                    pick_id=pick.resource_id,
                                    waveform_id=pick.waveform_id)
             # Amplitude for coda magnitude
@@ -953,7 +803,7 @@ def blanksfile(wavefile, evtype, userid, overwrite=False, evtime=None):
 
 
 def write_select(catalog, filename, userid='OBSP', evtype='L',
-                 wavefiles=None):
+                 wavefiles=None, high_accuracy=True):
     """
     Function to write a catalog to a select file in nordic format.
 
@@ -970,6 +820,10 @@ def write_select(catalog, filename, userid='OBSP', evtype='L',
     :param wavefiles:
         Waveforms to associate the events with, must be ordered in the same
          way as the events in the catalog.
+    :type high_accuracy: bool
+    :param high_accuracy:
+        Whether to output pick seconds at 6.3f (high_accuracy) or
+        5.2f (standard)
     """
     if not wavefiles:
         wavefiles = ['DUMMY' for _i in range(len(catalog))]
@@ -978,7 +832,7 @@ def write_select(catalog, filename, userid='OBSP', evtype='L',
             select = io.StringIO()
             _write_nordic(event=event, filename=None, userid=userid,
                           evtype=evtype, wavefiles=wavfile,
-                          string_io=select)
+                          string_io=select, high_accuracy=high_accuracy)
             select.seek(0)
             for line in select:
                 fout.write(line)
@@ -987,7 +841,7 @@ def write_select(catalog, filename, userid='OBSP', evtype='L',
 
 def _write_nordic(event, filename, userid='OBSP', evtype='L', outdir='.',
                   wavefiles='DUMMY', explosion=False,
-                  overwrite=True, string_io=None):
+                  overwrite=True, string_io=None, high_accuracy=True):
     """
     Write an :class:`~obspy.core.event.Event` to a nordic formatted s-file.
 
@@ -1015,6 +869,10 @@ def _write_nordic(event, filename, userid='OBSP', evtype='L', outdir='.',
     :param string_io:
         If given, will write to the StringIO object in memory rather than to
         the filename.
+    :type high_accuracy: bool
+    :param high_accuracy:
+        Whether to output pick seconds at 6.3f (high_accuracy) or
+        5.2f (standard)
 
     :returns: str: name of nordic file written
 
@@ -1215,7 +1073,7 @@ def _write_nordic(event, filename, userid='OBSP', evtype='L', outdir='.',
                 ' VELO AIN AR TRES W  DIS CAZ7\n')
     # Now call the populate sfile function
     if len(event.picks) > 0:
-        newpicks = '\n'.join(nordpick(event))
+        newpicks = '\n'.join(nordpick(event, high_accuracy=high_accuracy))
         sfile.write(newpicks + '\n')
         sfile.write('\n'.rjust(81))
     if not string_io:
@@ -1260,17 +1118,17 @@ def _write_moment_tensor_line(focal_mechanism):
     # Sort out the second line
     lines[1][1:3] = 'MT'
     lines[1][3:9] = _str_conv(
-        focal_mechanism.moment_tensor.tensor.m_rr, 3).rjust(6)
+        focal_mechanism.moment_tensor.tensor.m_rr, 3)[0:6].rjust(6)
     lines[1][10:16] = _str_conv(
-        focal_mechanism.moment_tensor.tensor.m_tt, 3).rjust(6)
+        focal_mechanism.moment_tensor.tensor.m_tt, 3)[0:6].rjust(6)
     lines[1][17:23] = _str_conv(
-        focal_mechanism.moment_tensor.tensor.m_pp, 3).rjust(6)
+        focal_mechanism.moment_tensor.tensor.m_pp, 3)[0:6].rjust(6)
     lines[1][24:30] = _str_conv(
-        focal_mechanism.moment_tensor.tensor.m_rt, 3).rjust(6)
+        focal_mechanism.moment_tensor.tensor.m_rt, 3)[0:6].rjust(6)
     lines[1][31:37] = _str_conv(
-        focal_mechanism.moment_tensor.tensor.m_rp, 3).rjust(6)
+        focal_mechanism.moment_tensor.tensor.m_rp, 3)[0:6].rjust(6)
     lines[1][38:44] = _str_conv(
-        focal_mechanism.moment_tensor.tensor.m_tp, 3).rjust(6)
+        focal_mechanism.moment_tensor.tensor.m_tp, 3)[0:6].rjust(6)
     if hasattr(magnitude, 'creation_info') and hasattr(
             magnitude.creation_info, 'agency_id'):
         lines[1][45:48] = magnitude.creation_info.agency_id.rjust(3)[0:3]
@@ -1343,12 +1201,16 @@ def _write_hyp_error_line(origin):
     return ''.join(error_line)
 
 
-def nordpick(event):
+def nordpick(event, high_accuracy=True):
     """
     Format picks in an :class:`~obspy.core.event.event.Event` to nordic.
 
     :type event: :class:`~obspy.core.event.event.Event`
     :param event: A single obspy event.
+    :type high_accuracy: bool
+    :param high_accuracy:
+        Whether to output pick seconds at 6.3f (high_accuracy) or
+        5.2f (standard).
 
     :returns: List of String
 
@@ -1361,62 +1223,53 @@ def nordpick(event):
         from the values stored in seisan.  Multiple weights are also
         not supported.
     """
+    # Nordic picks do not have a date associated with them - we need time
+    # relative to some origin time.
+    try:
+        origin = event.preferred_origin() or event.origins[0]
+        origin_date = origin.time.date
+    except IndexError:
+        origin = Origin()
+        origin_date = min([p.time for p in event.picks]).date
     pick_strings = []
+    if high_accuracy:
+        pick_rounding = 3
+    else:
+        pick_rounding = 2
     for pick in event.picks:
         if not pick.waveform_id:
             msg = ('No waveform id for pick at time %s, skipping' % pick.time)
             warnings.warn(msg)
             continue
-        # Convert string to short sting
-        if pick.onset == 'impulsive':
-            impulsivity = 'I'
-        elif pick.onset == 'emergent':
-            impulsivity = 'E'
-        else:
-            impulsivity = ' '
-
-        # Convert string to short string
-        if pick.polarity == 'positive':
-            polarity = 'C'
-        elif pick.polarity == 'negative':
-            polarity = 'D'
-        else:
-            polarity = ' '
+        impulsivity = _str_conv(INV_ONSET_MAPPING.get(pick.onset))
+        polarity = _str_conv(INV_POLARITY_MAPPING.get(pick.polarity))
         # Extract velocity: Note that horizontal slowness in quakeML is stored
         # as s/deg
-        if pick.horizontal_slowness is not None:
-            # velocity = 1.0 / pick.horizontal_slowness
-            velocity = ' '  # Currently this conversion is unsupported.
-        else:
-            velocity = ' '
-        # Extract azimuth
-        if pick.backazimuth is not None:
-            azimuth = pick.backazimuth
-        else:
-            azimuth = ' '
+        # if pick.horizontal_slowness is not None:
+        #     # velocity = 1.0 / pick.horizontal_slowness
+        #     velocity = ' '  # Currently this conversion is unsupported.
+        # else:
+        #     velocity = ' '
+        velocity = ' '
+        azimuth = _str_conv(pick.backazimuth)
         # Extract the correct arrival info for this pick - assuming only one
         # arrival per pick...
-        try:
-            origin = event.preferred_origin() or event.origins[0]
-            arrival = [arrival for arrival in origin.arrivals
-                       if arrival.pick_id == pick.resource_id]
-        except IndexError:
-            arrival = []
+        arrival = [arrival for arrival in origin.arrivals
+                   if arrival.pick_id == pick.resource_id]
         if len(arrival) > 0:
+            if len(arrival) > 1:
+                warnings.warn("Multiple arrivals for pick - only writing one")
             arrival = arrival[0]
             # Extract weight - should be stored as 0-4, or 9 for seisan.
-            if arrival.time_weight is not None:
-                weight = int(arrival.time_weight)
-            else:
-                weight = '0'
+            weight = _str_conv(int(arrival.time_weight or 0))
             # Extract azimuth residual
             if arrival.backazimuth_residual is not None:
-                azimuthres = int(arrival.backazimuth_residual)
+                azimuthres = _str_conv(int(arrival.backazimuth_residual))
             else:
                 azimuthres = ' '
             # Extract time residual
             if arrival.time_residual is not None:
-                timeres = arrival.time_residual
+                timeres = _str_conv(arrival.time_residual, rounded=2)
             else:
                 timeres = ' '
             # Extract distance
@@ -1425,30 +1278,22 @@ def nordpick(event):
                 if distance >= 100.0:
                     distance = str(_int_conv(distance))
                 elif 10.0 < distance < 100.0:
-                    distance = _str_conv(round(distance, 1), 1)
+                    distance = _str_conv(round(distance, 1), rounded=1)
                 elif distance < 10.0:
-                    distance = _str_conv(round(distance, 2), 2)
+                    distance = _str_conv(round(distance, 2), rounded=2)
                 else:
                     distance = _str_conv(distance, False)
             else:
                 distance = ' '
             # Extract CAZ
             if arrival.azimuth is not None:
-                caz = int(arrival.azimuth)
+                caz = _str_conv(int(arrival.azimuth))
             else:
                 caz = ' '
         else:
-            caz = ' '
-            distance = ' '
-            timeres = ' '
-            azimuthres = ' '
-            azimuth = ' '
-            weight = 0
-        if not pick.phase_hint:
-            # Cope with some authorities not providing phase hints :(
-            phase_hint = ' '
-        else:
-            phase_hint = pick.phase_hint
+            caz, distance, timeres, azimuthres, azimuth, weight = (
+                ' ', ' ', ' ', ' ', ' ', 0)
+        phase_hint = pick.phase_hint or ' '
         # Extract amplitude: note there can be multiple amplitudes, but they
         # should be associated with different picks.
         amplitude = [amplitude for amplitude in event.amplitudes
@@ -1496,38 +1341,58 @@ def nordpick(event):
             peri_round = False
             amp = None
             coda = ' '
-        # If the weight is 0 then we don't need to print it
-        if weight == 0 or weight == '0':
-            weight = None  # this will return an empty string using _str_conv
-        elif weight is not None:
-            weight = int(weight)
-        if pick.evaluation_mode == "automatic":
-            eval_mode = "A"
-        elif pick.evaluation_mode == "manual":
-            eval_mode = " "
-        else:
-            warnings.warn("Evaluation mode %s is not supported"
-                          % pick.evaluation_mode)
-            eval_mode = " "
+        eval_mode = INV_EVALUTATION_MAPPING.get(pick.evaluation_mode, None)
+        if eval_mode is None:
+            warnings.warn("Evaluation mode {0} is not mappable".format(
+                pick.evaluation_mode))
         # Generate a print string and attach it to the list
         channel_code = pick.waveform_id.channel_code or '   '
-        pick_strings.append(
-            " {0}{1}{2} {3}{4}{5}{6}{7} {8}{9}{10}.{11}{12}"
-            "{13}{14}{15}{16}    {17}{18}  {19}{20} ".format(
-                pick.waveform_id.station_code.ljust(5), channel_code[0],
-                channel_code[-1], impulsivity, phase_hint.ljust(4),
-                _str_conv(weight).rjust(1), eval_mode, polarity.rjust(1),
-                str(pick.time.hour).rjust(2), str(pick.time.minute).rjust(2),
-                str(pick.time.second).rjust(3),
-                str(float(pick.time.microsecond) /
-                    (10 ** 4)).split('.')[0].zfill(2),
-                _str_conv(coda).rjust(5)[0:5],
-                _str_conv(amp, rounded=1).rjust(7)[0:7],
-                _str_conv(peri, rounded=peri_round).rjust(5),
-                _str_conv(azimuth).rjust(6), _str_conv(velocity).rjust(5),
-                _str_conv(azimuthres).rjust(3),
-                _str_conv(timeres, rounded=2).rjust(5)[0:5], distance.rjust(5),
-                _str_conv(caz).rjust(4)))
+        pick_hour = pick.time.hour
+        if pick.time.date > origin_date:
+            # pick hours up to 48 are supported
+            days_diff = (pick.time.date - origin_date).days
+            if days_diff > 1:
+                raise NordicParsingError(
+                    "Pick is {0} days from the origin, must be < 48 "
+                    "hours".format(days_diff))
+            pick_hour += 24
+        pick_seconds = pick.time.second + (pick.time.microsecond / 1e6)
+        if len(phase_hint) > 4:
+            # Weight goes in 9 and phase_hint runs through 11-18
+            if polarity != ' ':
+                UserWarning("Polarity not written due to phase hint length")
+            phase_info = (
+                _str_conv(weight).rjust(1) + impulsivity + phase_hint.ljust(8))
+        else:
+            phase_info = (
+                ' ' + impulsivity + phase_hint.ljust(4) +
+                _str_conv(weight).rjust(1) + eval_mode +
+                polarity.rjust(1) + ' ')
+        pick_string_formatter = (
+            " {station:5s}{instrument:1s}{component:1s}{phase_info:10s}"
+            "{hour:2d}{minute:2d}{seconds:6s}{coda:5s}{amp:7s}{period:5s}"
+            "{azimuth:6s}{velocity:5s}    {azimuthres:3s}{timeres:5s}  "
+            "{distance:5s}{caz:4s} ")
+        # Note that pick seconds rounding only works because SEISAN does not
+        # enforce that seconds stay 0 <= seconds < 60, so rounding something
+        # like seconds = 59.997 to 2dp gets to 60.00, which SEISAN is happy
+        # with.  It appears that SEISAN is happy with large numbers of seconds
+        # see #2348.
+        pick_strings.append(pick_string_formatter.format(
+            station=pick.waveform_id.station_code,
+            instrument=channel_code[0], component=channel_code[-1],
+            phase_info=phase_info, hour=pick_hour,
+            minute=pick.time.minute,
+            seconds=_str_conv(pick_seconds, rounded=pick_rounding),
+            coda=_str_conv(coda).rjust(5)[0:5],
+            amp=_str_conv(amp, rounded=1).rjust(7)[0:7],
+            period=_str_conv(peri, rounded=peri_round).rjust(5)[0:5],
+            azimuth=_str_conv(azimuth).rjust(6)[0:6],
+            velocity=_str_conv(velocity).rjust(5)[0:5],
+            azimuthres=_str_conv(azimuthres).rjust(3)[0:3],
+            timeres=_str_conv(timeres, rounded=2).rjust(5)[0:5],
+            distance=distance.rjust(5)[0:5],
+            caz=_str_conv(caz).rjust(4)[0:4]))
         # Note that currently finalweight is unsupported, nor is velocity, or
         # angle of incidence.  This is because obspy.event stores slowness in
         # s/deg and takeoff angle, which would require computation from the
diff --git a/obspy/io/nordic/utils.py b/obspy/io/nordic/utils.py
new file mode 100644
--- /dev/null
+++ b/obspy/io/nordic/utils.py
@@ -0,0 +1,169 @@
+# -*- coding: utf-8 -*-
+"""
+Utility functions for Nordic file format support for ObsPy
+
+:copyright:
+    The ObsPy Development Team (devs@obspy.org)
+:license:
+    GNU Lesser General Public License, Version 3
+    (https://www.gnu.org/copyleft/lesser.html)
+"""
+from __future__ import (absolute_import, division, print_function,
+                        unicode_literals)
+from future.builtins import *  # NOQA @UnusedWildImport
+
+import warnings
+from collections import defaultdict
+
+from obspy.io.nordic import NordicParsingError
+
+
+MAG_MAPPING = {"ML": "L", "MLv": "L", "mB": "B", "Ms": "s", "MS": "S",
+               "MW": "W", "MbLg": "G", "Mc": "C"}
+INV_MAG_MAPPING = {item: key for key, item in MAG_MAPPING.items()}
+# List of currently implemented line-endings, which in Nordic mark what format
+# info in that line will be.
+ACCEPTED_TAGS = ('1', '6', '7', 'E', ' ', 'F', 'M', '3')
+
+
+def _int_conv(string):
+    """
+    Convenience tool to convert from string to integer.
+
+    If empty string return None rather than an error.
+
+    >>> _int_conv('12')
+    12
+    >>> _int_conv('')
+
+    """
+    try:
+        intstring = int(string)
+    except Exception:
+        intstring = None
+    return intstring
+
+
+def _float_conv(string):
+    """
+    Convenience tool to convert from string to float.
+
+    If empty string return None rather than an error.
+
+    >>> _float_conv('12')
+    12.0
+    >>> _float_conv('')
+    >>> _float_conv('12.324')
+    12.324
+    """
+    try:
+        floatstring = float(string)
+    except Exception:
+        floatstring = None
+    return floatstring
+
+
+def _str_conv(number, rounded=False):
+    """
+    Convenience tool to convert a number, either float or int into a string.
+
+    If the int or float is None, returns empty string.
+
+    >>> print(_str_conv(12.3))
+    12.3
+    >>> print(_str_conv(12.34546, rounded=1))
+    12.3
+    >>> print(_str_conv(None))
+    <BLANKLINE>
+    >>> print(_str_conv(1123040))
+    11.2e5
+    """
+    if not number:
+        return str(' ')
+    if not rounded and isinstance(number, (float, int)):
+        if number < 100000:
+            string = str(number)
+        else:
+            exponent = int('{0:.2E}'.format(number).split('E+')[-1]) - 1
+            divisor = 10 ** exponent
+            string = '{0:.1f}'.format(number / divisor) + 'e' + str(exponent)
+    elif rounded and isinstance(number, (float, int)):
+        if number < 100000:
+            string = "{:.{precision}f}".format(number, precision=rounded)
+        else:
+            exponent = int('{0:.2E}'.format(number).split('E+')[-1]) - 1
+            divisor = 10 ** exponent
+            string = "{:.{precision}f}".format(
+                number / divisor, precision=rounded) + 'e' + str(exponent)
+    else:
+        return str(number)
+    return string
+
+
+def _get_line_tags(f, report=True):
+    """
+    Associate lines with a known line-type
+    :param f: File open in read
+    :param report: Whether to report warnings about lines not implemented
+    """
+    f.seek(0)
+    line = f.readline()
+    if len(line.rstrip()) != 80:
+        # Cannot be Nordic
+        raise NordicParsingError(
+            "Lines are not 80 characters long: not a nordic file")
+    f.seek(0)
+    tags = defaultdict(list)
+    for i, line in enumerate(f):
+        try:
+            line_id = line.rstrip()[79]
+        except IndexError:
+            line_id = ' '
+        if line_id in ACCEPTED_TAGS:
+            tags[line_id].append((line, i))
+        elif report:
+            warnings.warn("Lines of type %s have not been implemented yet, "
+                          "please submit a development request" % line_id)
+    return tags
+
+
+def _evmagtonor(mag_type):
+    """
+    Switch from obspy event magnitude types to seisan syntax.
+
+    >>> print(_evmagtonor('mB'))  # doctest: +SKIP
+    B
+    >>> print(_evmagtonor('M'))  # doctest: +SKIP
+    W
+    >>> print(_evmagtonor('bob'))  # doctest: +SKIP
+    <BLANKLINE>
+    """
+    if mag_type == 'M':
+        warnings.warn('Converting generic magnitude to moment magnitude')
+        return "W"
+    mag = MAG_MAPPING.get(mag_type, '')
+    if mag == '':
+        warnings.warn(mag_type + ' is not convertible')
+    return mag
+
+
+def _nortoevmag(mag_type):
+    """
+    Switch from nordic type magnitude notation to obspy event magnitudes.
+
+    >>> print(_nortoevmag('B'))  # doctest: +SKIP
+    mB
+    >>> print(_nortoevmag('bob'))  # doctest: +SKIP
+    <BLANKLINE>
+    """
+    if mag_type.upper() == "L":
+        return "ML"
+    mag = INV_MAG_MAPPING.get(mag_type, '')
+    if mag == '':
+        warnings.warn(mag_type + ' is not convertible')
+    return mag
+
+
+if __name__ == "__main__":
+    import doctest
+    doctest.testmod()


Test patch:
diff --git a/obspy/io/nordic/tests/data/sfile_high_precision_picks b/obspy/io/nordic/tests/data/sfile_high_precision_picks
new file mode 100644
--- /dev/null
+++ b/obspy/io/nordic/tests/data/sfile_high_precision_picks
@@ -0,0 +1,14 @@
+ 2010 1126 0128 45.1 L  37.324 -32.293  2.0  MWW  4 0.0                        1
+ GAP=249        0.34       2.0     0.7  2.3 -0.4033E+00 -0.2372E+00  0.3948E+01E
+ 2010 1126 0128 45.148  37.32390  -32.29291    2.000  0.022                    H
+ ACTION:UP  19-02-22 14:32 OP:pb   STATUS:               ID:20101126012841     I
+ OLDACT:REG 19-02-22 14:31 OP:pb   STATUS:               ID:20101126012841     3
+ 2010-11-26-0128-41S.NSN___020                                                 6
+ STAT SP IPHASW D HRMM SECON CODA AMPLIT PERI AZIMU VELO AIN AR TRES W  DIS CAZ7
+ LSd1 SZ EPg       12846.859                             148    0.0110 1.34 110 
+ LSd3 SZ EPg       12848.132                             121    0.0310 3.81 163 
+ LSd2 SZ EPg       12848.183                             115   -0.0110 4.13 221 
+ LSd4 SZ EPg       12849.744                             106   -0.03 9 6.66 136 
+
+                                                                                                                                                               
+ 
\ No newline at end of file
diff --git a/obspy/io/nordic/tests/data/sfile_long_phase b/obspy/io/nordic/tests/data/sfile_long_phase
new file mode 100644
--- /dev/null
+++ b/obspy/io/nordic/tests/data/sfile_long_phase
@@ -0,0 +1,6 @@
+ 2010 1126 0128 45.1 L  37.324 -32.293  2.0  MWW  4 0.0                        1
+ STAT SP IPHASW D HRMM SECON CODA AMPLIT PERI AZIMU VELO AIN AR TRES W  DIS CAZ7
+ LSd1 SZ1EPKiKP    12846.859                                    0.0110 1.34 110 
+
+                                                                                                                                                               
+ 
\ No newline at end of file
diff --git a/obspy/io/nordic/tests/data/sfile_seconds_overflow b/obspy/io/nordic/tests/data/sfile_seconds_overflow
new file mode 100644
--- /dev/null
+++ b/obspy/io/nordic/tests/data/sfile_seconds_overflow
@@ -0,0 +1,8 @@
+ 2009  7 2 0650 38.0 L  37.204 -32.354  8.5  AKR  5 0.1 1.2LAKR                1
+ GAP=325        0.39       3.1     4.2  4.7 -0.2566E+01  0.3577E+01  0.5629E+01E
+ 2009  7 2 0650 37.984  37.20362  -32.35415    8.489  0.142                    H
+ ACTION:ARG 11-03-16 17:12 OP:wcc  STATUS:               ID:20090702065037 L   I
+ 2009-07-02-0649-56M.LSFIR_024                                                 6
+ STAT SP IPHASW D HRMM SECON CODA AMPLIT PERI AZIMU VELO AIN AR TRES W  DIS CAZ7
+ LSb2 SZ IP        649 100.24 129                        113    0.0210 10.9  14
+
diff --git a/obspy/io/nordic/tests/test_nordic.py b/obspy/io/nordic/tests/test_nordic.py
--- a/obspy/io/nordic/tests/test_nordic.py
+++ b/obspy/io/nordic/tests/test_nordic.py
@@ -21,10 +21,13 @@
     NodalPlane, NodalPlanes, ResourceIdentifier, Tensor)
 from obspy.core.util.base import NamedTemporaryFile
 from obspy.core.util.misc import TemporaryWorkingDirectory
+
+from obspy.io.nordic import NordicParsingError
 from obspy.io.nordic.core import (
     _is_sfile, read_spectral_info, read_nordic, readwavename, blanksfile,
-    _write_nordic, nordpick, readheader, _int_conv, _readheader, _evmagtonor,
-    write_select, NordicParsingError, _float_conv, _nortoevmag, _str_conv,
+    _write_nordic, nordpick, readheader, _readheader,  write_select)
+from obspy.io.nordic.utils import (
+    _int_conv, _float_conv, _str_conv, _nortoevmag, _evmagtonor,
     _get_line_tags)
 
 
@@ -293,8 +296,11 @@ def test_read_extra_header(self):
                          header_event.origins[0].depth)
 
     def test_header_mapping(self):
-        head_1 = readheader(os.path.join(self.testing_path,
-                                         '01-0411-15L.S201309'))
+        # Raise "UserWarning: Lines of type I..."
+        with warnings.catch_warnings():
+            warnings.simplefilter('ignore', UserWarning)
+            head_1 = readheader(os.path.join(self.testing_path,
+                                             '01-0411-15L.S201309'))
         with open(os.path.join(self.testing_path,
                                '01-0411-15L.S201309'), 'r') as f:
             # raises "UserWarning: AIN in header, currently unsupported"
@@ -307,7 +313,10 @@ def test_header_mapping(self):
     def test_missing_header(self):
         # Check that a suitable error is raised
         with self.assertRaises(NordicParsingError):
-            readheader(os.path.join(self.testing_path, 'Sfile_no_header'))
+            # Raises AIN warning
+            with warnings.catch_warnings():
+                warnings.simplefilter('ignore', UserWarning)
+                readheader(os.path.join(self.testing_path, 'Sfile_no_header'))
 
     def test_reading_string_io(self):
         filename = os.path.join(self.testing_path, '01-0411-15L.S201309')
@@ -427,19 +436,23 @@ def test_read_latin1(self):
         """
         Check that we can read dos formatted, latin1 encoded files.
         """
-        dos_file = os.path.join(self.testing_path, 'dos-file.sfile')
-        self.assertTrue(_is_sfile(dos_file))
-        event = readheader(dos_file)
-        self.assertEqual(event.origins[0].latitude, 60.328)
-        cat = read_events(dos_file)
-        self.assertEqual(cat[0].origins[0].latitude, 60.328)
-        wavefiles = readwavename(dos_file)
-        self.assertEqual(wavefiles[0], "90121311.0851W41")
-        spectral_info = read_spectral_info(dos_file)
-        self.assertEqual(len(spectral_info.keys()), 10)
-        self.assertEqual(spectral_info[('AVERAGE', '')]['stress_drop'], 27.7)
-        with self.assertRaises(UnicodeDecodeError):
-            readheader(dos_file, 'ASCII')
+        with warnings.catch_warnings():
+            # Lots of unsupported line warnings
+            warnings.simplefilter('ignore', UserWarning)
+            dos_file = os.path.join(self.testing_path, 'dos-file.sfile')
+            self.assertTrue(_is_sfile(dos_file))
+            event = readheader(dos_file)
+            self.assertEqual(event.origins[0].latitude, 60.328)
+            cat = read_events(dos_file)
+            self.assertEqual(cat[0].origins[0].latitude, 60.328)
+            wavefiles = readwavename(dos_file)
+            self.assertEqual(wavefiles[0], "90121311.0851W41")
+            spectral_info = read_spectral_info(dos_file)
+            self.assertEqual(len(spectral_info.keys()), 10)
+            self.assertEqual(spectral_info[('AVERAGE', '')]['stress_drop'],
+                             27.7)
+            with self.assertRaises(UnicodeDecodeError):
+                readheader(dos_file, 'ASCII')
 
     def test_read_many_events(self):
         testing_path = os.path.join(self.testing_path, 'select.out')
@@ -457,9 +470,12 @@ def test_write_select(self):
                 warnings.simplefilter('ignore', UserWarning)
                 write_select(cat, filename=tf.name)
             self.assertTrue(_is_sfile(tf.name))
-            cat_back = read_events(tf.name)
-            for event_1, event_2 in zip(cat, cat_back):
-                _assert_similarity(event_1=event_1, event_2=event_2)
+            with warnings.catch_warnings():
+                # Type I warning
+                warnings.simplefilter('ignore', UserWarning)
+                cat_back = read_events(tf.name)
+        for event_1, event_2 in zip(cat, cat_back):
+            _assert_similarity(event_1=event_1, event_2=event_2)
 
     def test_write_plugin(self):
         cat = read_events()
@@ -534,7 +550,10 @@ def test_read_moment_info(self):
         Test reading the info from spectral analysis.
         """
         testing_path = os.path.join(self.testing_path, 'automag.out')
-        spec_inf = read_spectral_info(testing_path)
+        with warnings.catch_warnings():
+            # Userwarning, type I
+            warnings.simplefilter('ignore', UserWarning)
+            spec_inf = read_spectral_info(testing_path)
         self.assertEqual(len(spec_inf), 5)
         # This should actually test that what we are reading in is correct.
         average = spec_inf[('AVERAGE', '')]
@@ -615,8 +634,10 @@ def test_large_negative_longitude(self):
         event.origins[0].longitude = -120
         with NamedTemporaryFile(suffix=".out") as tf:
             event.write(tf.name, format="NORDIC")
-            event_back = read_events(tf.name)
-            _assert_similarity(event, event_back[0])
+            with warnings.catch_warnings():
+                warnings.simplefilter('ignore', UserWarning)
+                event_back = read_events(tf.name)
+        _assert_similarity(event, event_back[0])
 
     def test_write_preferred_origin(self):
         event = full_test_event()
@@ -627,15 +648,152 @@ def test_write_preferred_origin(self):
         event.preferred_origin_id = preferred_origin.resource_id
         with NamedTemporaryFile(suffix=".out") as tf:
             event.write(tf.name, format="NORDIC")
-            event_back = read_events(tf.name)
-            self.assertEqual(preferred_origin.latitude,
-                             event_back[0].origins[0].latitude)
-            self.assertEqual(preferred_origin.longitude,
-                             event_back[0].origins[0].longitude)
-            self.assertEqual(preferred_origin.depth,
-                             event_back[0].origins[0].depth)
-            self.assertEqual(preferred_origin.time,
-                             event_back[0].origins[0].time)
+            with warnings.catch_warnings():
+                # Type I warning
+                warnings.simplefilter('ignore', UserWarning)
+                event_back = read_events(tf.name)
+        self.assertEqual(preferred_origin.latitude,
+                         event_back[0].origins[0].latitude)
+        self.assertEqual(preferred_origin.longitude,
+                         event_back[0].origins[0].longitude)
+        self.assertEqual(preferred_origin.depth,
+                         event_back[0].origins[0].depth)
+        self.assertEqual(preferred_origin.time,
+                         event_back[0].origins[0].time)
+
+    def test_read_high_precision_pick(self):
+        """
+        Nordic supports writing to milliseconds in high-precision mode,
+        obspy < 1.2.0 did not properly read this, see #2348.
+        """
+        # raises "UserWarning: AIN in header, currently unsupported"
+        with warnings.catch_warnings():
+            warnings.simplefilter('ignore', UserWarning)
+            cat = read_events(
+                os.path.join(self.testing_path, "sfile_high_precision_picks"))
+        event = cat[0]
+        pick_times = {
+            "LSd1": UTCDateTime(2010, 11, 26, 1, 28, 46.859),
+            "LSd3": UTCDateTime(2010, 11, 26, 1, 28, 48.132),
+            "LSd2": UTCDateTime(2010, 11, 26, 1, 28, 48.183),
+            "LSd4": UTCDateTime(2010, 11, 26, 1, 28, 49.744)}
+        for key, value in pick_times.items():
+            pick = [p for p in event.picks
+                    if p.waveform_id.station_code == key]
+            self.assertEqual(len(pick), 1)
+            self.assertEqual(pick[0].time, value)
+
+    def test_high_precision_read_write(self):
+        """ Test that high-precision writing works. """
+        # raises "UserWarning: AIN in header, currently unsupported"
+        with warnings.catch_warnings():
+            warnings.simplefilter('ignore', UserWarning)
+            cat = read_events(
+                os.path.join(self.testing_path, "sfile_high_precision_picks"))
+        event = cat[0]
+        pick_times = {
+            "LSd1": UTCDateTime(2010, 11, 26, 1, 28, 46.859),
+            "LSd3": UTCDateTime(2010, 11, 26, 1, 28, 48.132),
+            "LSd2": UTCDateTime(2010, 11, 26, 1, 28, 48.183),
+            "LSd4": UTCDateTime(2010, 11, 26, 1, 28, 49.744)}
+        for key, value in pick_times.items():
+            pick = [p for p in event.picks
+                    if p.waveform_id.station_code == key]
+            self.assertEqual(len(pick), 1)
+            self.assertEqual(pick[0].time, value)
+        with NamedTemporaryFile(suffix=".out") as tf:
+            write_select(cat, filename=tf.name)
+            with warnings.catch_warnings():
+                warnings.simplefilter('ignore', UserWarning)
+                cat_back = read_events(tf.name)
+        self.assertEqual(len(cat_back), 1)
+        for key, value in pick_times.items():
+            pick = [p for p in cat_back[0].picks
+                    if p.waveform_id.station_code == key]
+            self.assertEqual(len(pick), 1)
+            self.assertEqual(pick[0].time, value)
+        # Check that writing to standard accuracy just gives a rounded version
+        with NamedTemporaryFile(suffix=".out") as tf:
+            cat.write(format="NORDIC", filename=tf.name, high_accuracy=False)
+            with warnings.catch_warnings():
+                warnings.simplefilter('ignore', UserWarning)
+                cat_back = read_events(tf.name)
+        self.assertEqual(len(cat_back), 1)
+        for key, value in pick_times.items():
+            pick = [p for p in cat_back[0].picks
+                    if p.waveform_id.station_code == key]
+            self.assertEqual(len(pick), 1)
+            rounded_pick_time = UTCDateTime(
+                value.year, value.month, value.day, value.hour, value.minute)
+            rounded_pick_time += round(
+                value.second + (value.microsecond / 1e6), 2)
+            self.assertEqual(pick[0].time, rounded_pick_time)
+
+    def test_long_phase_name(self):
+        """ Nordic format supports 8 char phase names, sometimes. """
+        # raises "UserWarning: AIN in header, currently unsupported"
+        with warnings.catch_warnings():
+            warnings.simplefilter('ignore', UserWarning)
+            cat = read_events(
+                os.path.join(self.testing_path, "sfile_long_phase"))
+        # This file has one event with one pick
+        pick = cat[0].picks[0]
+        arrival = cat[0].origins[0].arrivals[0]
+        self.assertEqual(pick.phase_hint, "PKiKP")
+        self.assertEqual(arrival.time_weight, 1)
+        with NamedTemporaryFile(suffix=".out") as tf:
+            write_select(cat, filename=tf.name)
+            with warnings.catch_warnings():
+                warnings.simplefilter('ignore', UserWarning)
+                cat_back = read_events(tf.name)
+        pick = cat_back[0].picks[0]
+        arrival = cat_back[0].origins[0].arrivals[0]
+        self.assertEqual(pick.phase_hint, "PKiKP")
+        self.assertEqual(arrival.time_weight, 1)
+
+    def test_read_write_over_day(self):
+        """
+        Nordic picks are relative to origin time - check that this works
+        over day boundaries.
+        """
+        event = full_test_event()
+        event.origins[0].time -= 3600
+        self.assertGreater(
+            event.picks[0].time.date, event.origins[0].time.date)
+        with NamedTemporaryFile(suffix=".out") as tf:
+            write_select(Catalog([event]), filename=tf.name)
+            with warnings.catch_warnings():
+                warnings.simplefilter('ignore', UserWarning)
+                event_back = read_events(tf.name)[0]
+        _assert_similarity(event, event_back)
+
+    def test_seconds_overflow(self):
+        """
+        #2348 indicates that SEISAN sometimes overflows seconds into column 29.
+        """
+        with warnings.catch_warnings():
+            warnings.simplefilter('ignore', UserWarning)
+            cat = read_events(
+                os.path.join(self.testing_path, "sfile_seconds_overflow"))
+        event = cat[0]
+        pick_times = {
+            "LSb2": UTCDateTime(2009, 7, 2, 6, 49) + 100.24}
+        for key, value in pick_times.items():
+            pick = [p for p in event.picks
+                    if p.waveform_id.station_code == key]
+            self.assertEqual(len(pick), 1)
+            self.assertEqual(pick[0].time, value)
+        with NamedTemporaryFile(suffix=".out") as tf:
+            write_select(cat, filename=tf.name)
+            with warnings.catch_warnings():
+                warnings.simplefilter('ignore', UserWarning)
+                cat_back = read_events(tf.name)
+        self.assertEqual(len(cat_back), 1)
+        for key, value in pick_times.items():
+            pick = [p for p in cat_back[0].picks
+                    if p.waveform_id.station_code == key]
+            self.assertEqual(len(pick), 1)
+            self.assertEqual(pick[0].time, value)
 
 
 def _assert_similarity(event_1, event_2, verbose=False):
@@ -691,11 +849,11 @@ def _test_similarity(event_1, event_2, verbose=False):
                                 return False
                     if arr_1["distance"] and round(
                             arr_1["distance"]) != round(arr_2["distance"]):
-                            if verbose:
-                                print('%s does not match %s for key %s' %
-                                      (arr_1[arr_key], arr_2[arr_key],
-                                       arr_key))
-                            return False
+                        if verbose:
+                            print('%s does not match %s for key %s' %
+                                  (arr_1[arr_key], arr_2[arr_key],
+                                   arr_key))
+                        return False
     # Check picks
     if len(event_1.picks) != len(event_2.picks):
         if verbose: