1+ // Copyright 2019-2025 Chris Mohan, Jaben Cargman
2+ // and GotenbergSharpApiClient Contributors
3+ //
4+ // Licensed under the Apache License, Version 2.0 (the "License");
5+ // you may not use this file except in compliance with the License.
6+ // You may obtain a copy of the License at
7+ //
8+ // http://www.apache.org/licenses/LICENSE-2.0
9+ //
10+ // Unless required by applicable law or agreed to in writing, software
11+ // distributed under the License is distributed on an "AS IS" BASIS,
12+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+ // See the License for the specific language governing permissions and
14+ // limitations under the License.
15+
16+ using System . Text . RegularExpressions ;
17+
18+ namespace Gotenberg . Sharp . API . Client . Domain . Pages ;
19+
20+ public sealed class PageRanges : IEquatable < PageRanges >
21+ {
22+ private static readonly Regex PageRangePattern =
23+ new ( @"^\s*(\d+(-\d+)?)(\s*,\s*(\d+(-\d+)?))*\s*$" ) ;
24+
25+ private PageRanges ( IReadOnlyCollection < int > pages )
26+ {
27+ Pages = pages . OrderBy ( p => p ) . ToArray ( ) ;
28+ }
29+
30+ public static PageRanges All { get ; } = new ( Array . Empty < int > ( ) ) ;
31+
32+ public IReadOnlyCollection < int > Pages { get ; }
33+
34+ public bool Equals ( PageRanges ? other )
35+ {
36+ return other is not null && Pages . SequenceEqual ( other . Pages ) ;
37+ }
38+
39+ public static PageRanges Create ( string ? input )
40+ {
41+ if ( string . IsNullOrWhiteSpace ( input ) )
42+ {
43+ return All ;
44+ }
45+
46+ if ( ! PageRangePattern . IsMatch ( input ) )
47+ {
48+ throw new ArgumentOutOfRangeException ( nameof ( input ) ,
49+ "Invalid page range format. Expected format: '1-5, 8, 11-13'." ) ;
50+ }
51+
52+ var pages = new SortedSet < int > ( ) ;
53+ foreach ( var part in input ! . Split ( [ ',' ] , StringSplitOptions . RemoveEmptyEntries ) )
54+ {
55+ var trimmed = part . Trim ( ) ;
56+ if ( trimmed . Contains ( '-' ) )
57+ {
58+ var bounds = trimmed . Split ( '-' ) . Select ( int . Parse ) . ToArray ( ) ;
59+ if ( bounds . Length == 2 && bounds [ 0 ] <= bounds [ 1 ] )
60+ {
61+ for ( var i = bounds [ 0 ] ; i <= bounds [ 1 ] ; i ++ )
62+ {
63+ pages . Add ( i ) ;
64+ }
65+ }
66+ }
67+ else if ( int . TryParse ( trimmed , out var singlePage ) )
68+ {
69+ pages . Add ( singlePage ) ;
70+ }
71+ }
72+
73+ return new PageRanges ( pages ) ;
74+ }
75+
76+ private string GetPageRangeString ( )
77+ {
78+ // empty is "all"
79+ if ( Pages . Count == 0 )
80+ {
81+ return "" ;
82+ }
83+
84+ var ranges = new List < string > ( ) ;
85+ int start = Pages . First ( ) , end = start ;
86+
87+ foreach ( var page in Pages . Skip ( 1 ) )
88+ {
89+ if ( page == end + 1 )
90+ {
91+ end = page ;
92+ }
93+ else
94+ {
95+ ranges . Add ( start == end ? start . ToString ( ) : $ "{ start } -{ end } ") ;
96+ start = end = page ;
97+ }
98+ }
99+
100+ ranges . Add ( start == end ? start . ToString ( ) : $ "{ start } -{ end } ") ;
101+ return string . Join ( ", " , ranges ) ;
102+ }
103+
104+ public override string ToString ( )
105+ {
106+ return GetPageRangeString ( ) ;
107+ }
108+
109+ public override bool Equals ( object ? obj )
110+ {
111+ return obj is PageRanges other && Pages . SequenceEqual ( other . Pages ) ;
112+ }
113+
114+ public override int GetHashCode ( )
115+ {
116+ return Pages . Aggregate ( 0 , ( hash , page ) => hash ^ page . GetHashCode ( ) ) ;
117+ }
118+ }
0 commit comments