ZonedDateTime vs OffsetDateTime in Java

Overview

Both ZonedDateTime and OffsetDateTime are part of the Java Time API (java.time package) introduced in Java 8. They represent date-time with timezone information, but serve different purposes and have distinct characteristics.

Key Differences

AspectZonedDateTimeOffsetDateTime
Primary PurposeHandles timezone rules (DST, historical changes)Handles fixed timezone offsets
Timezone InfoFull timezone ID (e.g., "America/New_York")Fixed offset (e.g., "-05:00")
DST HandlingAutomatic DST adjustmentsNo DST awareness
Use CaseLocal time representation with timezone rulesTimestamp storage, inter-system communication
ComplexityMore complex, handles timezone rulesSimpler, fixed offset

Basic Usage Examples

1. Creation and Instantiation

import java.time.*;
import java.time.format.DateTimeFormatter;
import java.util.Set;
public class DateTimeCreationExamples {
public static void main(String[] args) {
// ZonedDateTime creation
ZonedDateTime zonedNow = ZonedDateTime.now();
ZonedDateTime zonedSpecific = ZonedDateTime.of(2024, 1, 15, 14, 30, 0, 0, 
ZoneId.of("America/New_York"));
ZonedDateTime zonedFromLocal = LocalDateTime.now().atZone(ZoneId.of("Europe/London"));
// OffsetDateTime creation
OffsetDateTime offsetNow = OffsetDateTime.now();
OffsetDateTime offsetSpecific = OffsetDateTime.of(2024, 1, 15, 14, 30, 0, 0, 
ZoneOffset.ofHours(-5));
OffsetDateTime offsetFromInstant = Instant.now().atOffset(ZoneOffset.UTC);
System.out.println("ZonedDateTime: " + zonedNow);
System.out.println("OffsetDateTime: " + offsetNow);
}
public static void demonstrateCreationMethods() {
// Various ways to create ZonedDateTime
ZonedDateTime zdt1 = ZonedDateTime.now();
ZonedDateTime zdt2 = ZonedDateTime.now(ZoneId.of("Asia/Tokyo"));
ZonedDateTime zdt3 = ZonedDateTime.of(2024, 1, 15, 10, 30, 0, 0, 
ZoneId.of("Europe/Paris"));
ZonedDateTime zdt4 = LocalDateTime.parse("2024-01-15T10:30:00")
.atZone(ZoneId.of("America/Los_Angeles"));
ZonedDateTime zdt5 = Instant.now().atZone(ZoneId.systemDefault());
// Various ways to create OffsetDateTime
OffsetDateTime odt1 = OffsetDateTime.now();
OffsetDateTime odt2 = OffsetDateTime.now(ZoneOffset.UTC);
OffsetDateTime odt3 = OffsetDateTime.of(2024, 1, 15, 10, 30, 0, 0, 
ZoneOffset.ofHours(2));
OffsetDateTime odt4 = LocalDateTime.parse("2024-01-15T10:30:00")
.atOffset(ZoneOffset.of("-06:00"));
OffsetDateTime odt5 = Instant.now().atOffset(ZoneOffset.ofHours(5));
System.out.println("ZonedDateTime examples:");
System.out.println("  System default: " + zdt1);
System.out.println("  Tokyo: " + zdt2);
System.out.println("  Paris: " + zdt3);
System.out.println("OffsetDateTime examples:");
System.out.println("  System default: " + odt1);
System.out.println("  UTC: " + odt2);
System.out.println("  +02:00: " + odt3);
}
}

2. Available Timezones and Offsets

public class TimezoneExplorer {
public static void exploreTimezones() {
// Get all available zone IDs
Set<String> allZones = ZoneId.getAvailableZoneIds();
System.out.println("Total timezones available: " + allZones.size());
// Display some common timezones
String[] commonZones = {
"America/New_York", "Europe/London", "Asia/Tokyo", 
"Australia/Sydney", "Africa/Cairo", "Pacific/Auckland"
};
System.out.println("\nCommon timezones and their current offsets:");
for (String zoneId : commonZones) {
ZoneId zone = ZoneId.of(zoneId);
ZonedDateTime now = ZonedDateTime.now(zone);
System.out.printf("  %-20s: %s (UTC%s)%n", 
zoneId, now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")),
now.getOffset());
}
// Show different offsets
System.out.println("\nCommon fixed offsets:");
ZoneOffset[] commonOffsets = {
ZoneOffset.UTC,
ZoneOffset.ofHours(-5),  // EST
ZoneOffset.ofHours(1),   // CET
ZoneOffset.ofHours(9),   // JST
ZoneOffset.of("+05:30")  // India
};
for (ZoneOffset offset : commonOffsets) {
OffsetDateTime now = OffsetDateTime.now(offset);
System.out.printf("  UTC%s: %s%n", 
offset, now.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME));
}
}
public static void checkDaylightSavingTime() {
// Check DST transitions for a specific timezone
ZoneId nyZone = ZoneId.of("America/New_York");
// Dates around DST transition
LocalDateTime beforeDST = LocalDateTime.of(2024, 3, 10, 1, 30); // Before spring forward
LocalDateTime afterDST = LocalDateTime.of(2024, 3, 10, 3, 30);  // After spring forward
ZonedDateTime zdtBefore = ZonedDateTime.of(beforeDST, nyZone);
ZonedDateTime zdtAfter = ZonedDateTime.of(afterDST, nyZone);
System.out.println("DST Transition in New York (Spring 2024):");
System.out.println("  Before DST: " + zdtBefore + " (Offset: " + zdtBefore.getOffset() + ")");
System.out.println("  After DST:  " + zdtAfter + " (Offset: " + zdtAfter.getOffset() + ")");
// Using OffsetDateTime (no DST awareness)
OffsetDateTime odtBefore = OffsetDateTime.of(beforeDST, ZoneOffset.ofHours(-5));
OffsetDateTime odtAfter = OffsetDateTime.of(afterDST, ZoneOffset.ofHours(-4));
System.out.println("Fixed offsets (no DST awareness):");
System.out.println("  Fixed EST: " + odtBefore);
System.out.println("  Fixed EDT: " + odtAfter);
}
}

Practical Use Cases

1. ZonedDateTime for User-Facing Times

public class UserFacingTimeExamples {
// Use ZonedDateTime when dealing with user local times
public static class EventScheduler {
private String eventName;
private ZonedDateTime eventTime;
public EventScheduler(String eventName, ZonedDateTime eventTime) {
this.eventName = eventName;
this.eventTime = eventTime;
}
public void displayEventInDifferentTimezones() {
System.out.println("Event: " + eventName);
System.out.println("Original time: " + formatForDisplay(eventTime));
// Convert to different timezones for participants
ZoneId[] participantZones = {
ZoneId.of("Europe/London"),
ZoneId.of("Asia/Tokyo"),
ZoneId.of("America/Los_Angeles")
};
for (ZoneId zone : participantZones) {
ZonedDateTime localTime = eventTime.withZoneSameInstant(zone);
System.out.println("  " + zone + ": " + formatForDisplay(localTime));
}
}
public boolean isBusinessHours() {
// Check if event is during business hours (9 AM - 5 PM) in its timezone
int hour = eventTime.getHour();
return hour >= 9 && hour < 17;
}
public void handleDSTTransition() {
// ZonedDateTime automatically handles DST
ZoneId zone = eventTime.getZone();
ZoneRules rules = zone.getRules();
if (rules.isDaylightSavings(eventTime.toInstant())) {
System.out.println("Event occurs during Daylight Saving Time");
System.out.println("DST offset: " + rules.getDaylightSavings(eventTime.toInstant()));
} else {
System.out.println("Event occurs during Standard Time");
}
}
private String formatForDisplay(ZonedDateTime zdt) {
return zdt.format(DateTimeFormatter.ofPattern("EEE, MMM d, yyyy 'at' h:mm a z"));
}
}
public static void demonstrateEventScheduling() {
// Schedule a meeting in New York
ZonedDateTime meetingTime = ZonedDateTime.of(
LocalDateTime.of(2024, 6, 15, 14, 0), // June 15, 2024, 2:00 PM
ZoneId.of("America/New_York")
);
EventScheduler meeting = new EventScheduler("Team Meeting", meetingTime);
meeting.displayEventInDifferentTimezones();
System.out.println("During business hours: " + meeting.isBusinessHours());
meeting.handleDSTTransition();
}
}

2. OffsetDateTime for System Operations

public class SystemTimeExamples {
// Use OffsetDateTime for timestamps, logging, and system communication
public static class SystemLogger {
private OffsetDateTime logTimestamp;
private String logMessage;
private ZoneOffset systemOffset;
public SystemLogger(String message) {
this.systemOffset = ZoneOffset.systemDefault().getRules()
.getOffset(Instant.now());
this.logTimestamp = OffsetDateTime.now(systemOffset);
this.logMessage = message;
}
public String getLogEntry() {
// Use ISO format for consistent parsing
return String.format("[%s] %s", 
logTimestamp.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME),
logMessage);
}
public static OffsetDateTime parseLogTimestamp(String logEntry) {
// Parse back from ISO format
String timestamp = logEntry.substring(1, logEntry.indexOf(']'));
return OffsetDateTime.parse(timestamp, DateTimeFormatter.ISO_OFFSET_DATE_TIME);
}
}
public static class DatabaseService {
// Use OffsetDateTime for database timestamps
public void saveRecord(String data, OffsetDateTime timestamp) {
// Convert to UTC for storage
OffsetDateTime utcTimestamp = timestamp.withOffsetSameInstant(ZoneOffset.UTC);
System.out.println("Saving record:");
System.out.println("  Data: " + data);
System.out.println("  Original: " + timestamp);
System.out.println("  UTC: " + utcTimestamp);
// Store in database (simulated)
// database.insert("records", data, utcTimestamp);
}
public void retrieveAndDisplay(String data, ZoneId userZone) {
// Retrieve from database (UTC)
OffsetDateTime utcTimestamp = OffsetDateTime.now(ZoneOffset.UTC);
// Convert to user's timezone for display
ZonedDateTime userTime = utcTimestamp.atZoneSameInstant(userZone);
System.out.println("Retrieved record for " + userZone + ":");
System.out.println("  Data: " + data);
System.out.println("  User local time: " + formatForUser(userTime));
}
private String formatForUser(ZonedDateTime zdt) {
return zdt.format(DateTimeFormatter.ofPattern("MMM d, yyyy h:mm a z"));
}
}
public static void demonstrateSystemOperations() {
// Logging with OffsetDateTime
SystemLogger logger = new SystemLogger("System started successfully");
System.out.println("Log entry: " + logger.getLogEntry());
// Database operations
DatabaseService db = new DatabaseService();
OffsetDateTime recordTime = OffsetDateTime.now();
db.saveRecord("Important data", recordTime);
db.retrieveAndDisplay("Important data", ZoneId.of("Europe/Berlin"));
}
}

Conversion and Interoperability

1. Conversion Between Types

public class DateTimeConversion {
public static void demonstrateConversions() {
// Current time instances
ZonedDateTime zoned = ZonedDateTime.now(ZoneId.of("America/Chicago"));
OffsetDateTime offset = OffsetDateTime.now(ZoneOffset.ofHours(-6));
Instant instant = Instant.now();
LocalDateTime local = LocalDateTime.now();
System.out.println("Original values:");
System.out.println("  ZonedDateTime: " + zoned);
System.out.println("  OffsetDateTime: " + offset);
System.out.println("  Instant: " + instant);
System.out.println("  LocalDateTime: " + local);
// Conversions from ZonedDateTime
System.out.println("\nFrom ZonedDateTime:");
System.out.println("  to OffsetDateTime: " + zoned.toOffsetDateTime());
System.out.println("  to Instant: " + zoned.toInstant());
System.out.println("  to LocalDateTime: " + zoned.toLocalDateTime());
// Conversions from OffsetDateTime
System.out.println("\nFrom OffsetDateTime:");
System.out.println("  to Instant: " + offset.toInstant());
System.out.println("  to LocalDateTime: " + offset.toLocalDateTime());
System.out.println("  to ZonedDateTime: " + offset.toZonedDateTime());
// Creating from Instant
System.out.println("\nFrom Instant:");
System.out.println("  to ZonedDateTime: " + instant.atZone(ZoneId.of("Europe/Paris")));
System.out.println("  to OffsetDateTime: " + instant.atOffset(ZoneOffset.ofHours(2)));
}
public static void handleConversionScenarios() {
// Scenario 1: User input to system storage
ZonedDateTime userInput = ZonedDateTime.of(2024, 12, 25, 20, 0, 0, 0, 
ZoneId.of("America/New_York"));
// Convert to UTC for storage
Instant instant = userInput.toInstant();
OffsetDateTime stored = instant.atOffset(ZoneOffset.UTC);
System.out.println("Storage scenario:");
System.out.println("  User input: " + userInput);
System.out.println("  Stored (UTC): " + stored);
// Scenario 2: Retrieval and display for different user
ZoneId userZone = ZoneId.of("Asia/Tokyo");
ZonedDateTime displayTime = stored.atZoneSameInstant(userZone);
System.out.println("  Display (Tokyo): " + displayTime);
// Scenario 3: Fixed offset conversion (e.g., for legacy systems)
OffsetDateTime fixedOffset = userInput.toOffsetDateTime();
System.out.println("  Fixed offset: " + fixedOffset);
}
public static void demonstrateWithHistoricalDate() {
// Historical date with timezone changes
LocalDateTime historicalLocal = LocalDateTime.of(1950, 1, 1, 12, 0);
ZoneId londonZone = ZoneId.of("Europe/London");
ZonedDateTime historicalZoned = ZonedDateTime.of(historicalLocal, londonZone);
OffsetDateTime historicalOffset = OffsetDateTime.of(historicalLocal, 
historicalZoned.getOffset());
System.out.println("Historical date (1950) in London:");
System.out.println("  ZonedDateTime: " + historicalZoned);
System.out.println("  OffsetDateTime: " + historicalOffset);
System.out.println("  Offset: " + historicalZoned.getOffset());
// Compare with current rules
ZonedDateTime currentZoned = ZonedDateTime.now(londonZone);
System.out.println("Current time in London:");
System.out.println("  ZonedDateTime: " + currentZoned);
System.out.println("  Offset: " + currentZoned.getOffset());
}
}

Performance and Best Practices

1. Performance Considerations

public class DateTimePerformance {
private static final int ITERATIONS = 100000;
public static void performanceComparison() {
// Creation performance
long startTime = System.nanoTime();
for (int i = 0; i < ITERATIONS; i++) {
ZonedDateTime zdt = ZonedDateTime.now();
}
long zonedTime = System.nanoTime() - startTime;
startTime = System.nanoTime();
for (int i = 0; i < ITERATIONS; i++) {
OffsetDateTime odt = OffsetDateTime.now();
}
long offsetTime = System.nanoTime() - startTime;
System.out.println("Creation Performance:");
System.out.printf("  ZonedDateTime: %d ns%n", zonedTime / ITERATIONS);
System.out.printf("  OffsetDateTime: %d ns%n", offsetTime / ITERATIONS);
// Conversion performance
ZonedDateTime zdt = ZonedDateTime.now();
startTime = System.nanoTime();
for (int i = 0; i < ITERATIONS; i++) {
Instant instant = zdt.toInstant();
}
long conversionTime = System.nanoTime() - startTime;
System.out.printf("  ZonedDateTime to Instant: %d ns%n", conversionTime / ITERATIONS);
}
public static void memoryUsageComparison() {
// Note: Actual memory usage would require more sophisticated measurement
System.out.println("\nMemory Considerations:");
System.out.println("  ZonedDateTime: Carries ZoneId (timezone rules)");
System.out.println("  OffsetDateTime: Carries only ZoneOffset (fixed offset)");
System.out.println("  OffsetDateTime is generally more memory-efficient");
}
}
public class DateTimeBestPractices {
public static class BestPracticeExamples {
// 1. Use ZonedDateTime for user-facing applications
public void displayUserSchedule(ZonedDateTime userTime) {
// Always display times in user's context
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("EEE, MMM d, yyyy 'at' h:mm a z");
System.out.println("Your event: " + userTime.format(formatter));
}
// 2. Use OffsetDateTime for system timestamps
public void logSystemEvent(String event) {
OffsetDateTime timestamp = OffsetDateTime.now(ZoneOffset.UTC);
// Log in consistent UTC format
System.out.println("[" + timestamp + "] " + event);
}
// 3. Store as Instant or UTC OffsetDateTime
public void saveToDatabase(ZonedDateTime userTime) {
// Convert to UTC for storage
Instant instant = userTime.toInstant();
OffsetDateTime utcTime = instant.atOffset(ZoneOffset.UTC);
// Store utcTime in database
System.out.println("Storing: " + utcTime);
}
// 4. Be careful with comparisons
public void compareTimesSafely() {
ZonedDateTime time1 = ZonedDateTime.now(ZoneId.of("America/New_York"));
ZonedDateTime time2 = ZonedDateTime.now(ZoneId.of("America/Los_Angeles"));
// Wrong: Direct comparison considers timezone
System.out.println("Direct equals: " + time1.equals(time2));
// Correct: Compare instants
System.out.println("Instant equals: " + time1.toInstant().equals(time2.toInstant()));
// Or convert to same timezone
System.out.println("Same zone equals: " + 
time1.equals(time2.withZoneSameInstant(time1.getZone())));
}
// 5. Handle DST transitions properly
public void handleAmbiguousTimes() {
ZoneId nyZone = ZoneId.of("America/New_York");
// During DST fall back, some local times are ambiguous
LocalDateTime ambiguousTime = LocalDateTime.of(2024, 11, 3, 1, 30);
try {
ZonedDateTime zdt = ZonedDateTime.of(ambiguousTime, nyZone);
System.out.println("Resolved time: " + zdt);
} catch (Exception e) {
System.out.println("Ambiguous time detected: " + e.getMessage());
// Handle by specifying offset preference
ZonedDateTime early = ambiguousTime.atZone(nyZone).withEarlierOffsetAtOverlap();
ZonedDateTime late = ambiguousTime.atZone(nyZone).withLaterOffsetAtOverlap();
System.out.println("Earlier offset: " + early);
System.out.println("Later offset: " + late);
}
}
}
public static void demonstrateBestPractices() {
BestPracticeExamples examples = new BestPracticeExamples();
examples.displayUserSchedule(ZonedDateTime.now(ZoneId.of("Europe/Paris")));
examples.logSystemEvent("Application started");
examples.saveToDatabase(ZonedDateTime.now());
examples.compareTimesSafely();
examples.handleAmbiguousTimes();
}
}

Real-World Scenarios

1. Flight Scheduling System

public class FlightSchedulingSystem {
public static class Flight {
private String flightNumber;
private ZonedDateTime departure;
private ZonedDateTime arrival;
private Duration flightDuration;
public Flight(String flightNumber, ZonedDateTime departure, ZonedDateTime arrival) {
this.flightNumber = flightNumber;
this.departure = departure;
this.arrival = arrival;
this.flightDuration = Duration.between(departure.toInstant(), arrival.toInstant());
}
public void displayFlightInfo() {
System.out.println("Flight " + flightNumber + ":");
System.out.println("  Depart: " + formatZoned(departure));
System.out.println("  Arrive: " + formatZoned(arrival));
System.out.println("  Duration: " + formatDuration(flightDuration));
// Show in passenger's local timezone
displayForPassenger(ZoneId.of("Asia/Tokyo"));
}
private void displayForPassenger(ZoneId passengerZone) {
ZonedDateTime passengerDeparture = departure.withZoneSameInstant(passengerZone);
ZonedDateTime passengerArrival = arrival.withZoneSameInstant(passengerZone);
System.out.println("  For passenger in " + passengerZone + ":");
System.out.println("    Depart: " + formatZoned(passengerDeparture));
System.out.println("    Arrive: " + formatZoned(passengerArrival));
}
public boolean isValidFlight() {
// Ensure arrival is after departure
return arrival.toInstant().isAfter(departure.toInstant());
}
public void handleDaylightSaving() {
ZoneId departureZone = departure.getZone();
ZoneRules rules = departureZone.getRules();
if (rules.isDaylightSavings(departure.toInstant()) != 
rules.isDaylightSavings(arrival.toInstant())) {
System.out.println("  Note: Flight crosses DST boundary");
}
}
private String formatZoned(ZonedDateTime zdt) {
return zdt.format(DateTimeFormatter.ofPattern("MMM d, yyyy h:mm a z"));
}
private String formatDuration(Duration duration) {
long hours = duration.toHours();
long minutes = duration.toMinutesPart();
return String.format("%dh %dm", hours, minutes);
}
}
public static void demonstrateFlightSystem() {
// Flight from New York to London
ZonedDateTime depart = ZonedDateTime.of(
LocalDateTime.of(2024, 6, 15, 20, 0),
ZoneId.of("America/New_York")
);
ZonedDateTime arrive = ZonedDateTime.of(
LocalDateTime.of(2024, 6, 16, 8, 0),
ZoneId.of("Europe/London")
);
Flight flight = new Flight("BA178", depart, arrive);
flight.displayFlightInfo();
System.out.println("Valid flight: " + flight.isValidFlight());
flight.handleDaylightSaving();
}
}

2. Global Trading System

public class TradingSystem {
public static class Trade {
private String symbol;
private double price;
private OffsetDateTime tradeTime; // Use OffsetDateTime for precise timestamps
private String exchange;
public Trade(String symbol, double price, OffsetDateTime tradeTime, String exchange) {
this.symbol = symbol;
this.price = price;
this.tradeTime = tradeTime;
this.exchange = exchange;
}
public boolean isMarketHours() {
// Convert to exchange local time to check market hours
ZoneId exchangeZone = getExchangeZone(exchange);
ZonedDateTime exchangeTime = tradeTime.atZoneSameInstant(exchangeZone);
int hour = exchangeTime.getHour();
return hour >= 9 && hour < 16; // 9 AM to 4 PM
}
public void displayTrade() {
System.out.println("Trade: " + symbol + " @ " + price);
System.out.println("  Time (UTC): " + tradeTime);
// Display in various timezones
displayInTimeZone(ZoneId.of("America/New_York"), "NY Trader");
displayInTimeZone(ZoneId.of("Europe/London"), "London Trader");
displayInTimeZone(ZoneId.of("Asia/Tokyo"), "Tokyo Trader");
System.out.println("  During market hours: " + isMarketHours());
}
private void displayInTimeZone(ZoneId zone, String user) {
ZonedDateTime localTime = tradeTime.atZoneSameInstant(zone);
String formatted = localTime.format(DateTimeFormatter.ofPattern("MMM d, h:mm a z"));
System.out.println("  " + user + ": " + formatted);
}
private ZoneId getExchangeZone(String exchange) {
switch (exchange) {
case "NYSE": return ZoneId.of("America/New_York");
case "LSE": return ZoneId.of("Europe/London");
case "TSE": return ZoneId.of("Asia/Tokyo");
default: return ZoneOffset.UTC;
}
}
}
public static void demonstrateTradingSystem() {
// Record a trade with UTC timestamp
OffsetDateTime tradeTime = OffsetDateTime.now(ZoneOffset.UTC);
Trade trade = new Trade("AAPL", 150.25, tradeTime, "NYSE");
trade.displayTrade();
// Compare trades from different exchanges
OffsetDateTime tokyoTradeTime = OffsetDateTime.now(ZoneOffset.ofHours(9));
Trade tokyoTrade = new Trade("7203.T", 2500.0, tokyoTradeTime, "TSE");
System.out.println("\nTokyo Trade:");
tokyoTrade.displayTrade();
// Check if trades occurred simultaneously
System.out.println("Trades simultaneous: " + 
trade.tradeTime.toInstant().equals(tokyoTrade.tradeTime.toInstant()));
}
}

Summary Guidelines

When to Use ZonedDateTime:

  • User-facing applications where local time matters
  • Scheduling systems that need to handle DST
  • Calendar applications with timezone-aware events
  • International applications displaying times in user's local timezone

When to Use OffsetDateTime:

  • System timestamps and logging
  • Database storage (prefer UTC)
  • API communications between systems
  • Financial transactions requiring precise timestamps
  • When you only need fixed offset without timezone rules

General Rules:

  1. Store as Instant or UTC OffsetDateTime
  2. Display as ZonedDateTime in user's timezone
  3. Convert at system boundaries (API, database, UI)
  4. Use fixed offsets when timezone rules don't matter
  5. Always consider DST when working with local times

Choosing between ZonedDateTime and OffsetDateTime depends on whether you need full timezone rules (including DST) or just a fixed offset from UTC. Understanding this distinction is crucial for building correct international applications.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper