User/In/Out is, IMO, the better option. It can easily deal with unmatched punches by using the following algorithm:
Punch In:
- Always create a new record.
Punch Out:
- If user's most recent Punch In has no attached Punch Out, record current time as Punch Out on that record.
- Otherwise, create a new record.
Calculating logged time is now trivial (add up Punch Out minus Punch In for each record), as is identifying unmatched punches (look for any records where either field is null), all without needing to scan the table to try to match everything up with temporally-adjacent records every time you examine them.
Granted, there will be cases when a Punch Out may end up being associated with the wrong Punch In - but the same sequence of punches would produce the same result when using the other approach unless it uses a smarter algorithm to match them up. When users don't provide a strictly alternating In/Out/In/Out... sequence, getting them matched up correctly is purely a matter of the algorithm used to do the matching, not of the data structure used to store the punches.