Skip to content

Commit 8a4075e

Browse files
committed
feat: Add Decimal128Value (#799)
* WIP: Decimal128Value. * WIP: decimal128 values. next: add quadruple + compare. next: add tests. * Add quadruple and quadruple_builder. * Implement comparison logic. * Fix bug and add integration tests. * Add unit tests. * clang-format. * Fix the NumberEquals logic. * Port the missing tests from Android. * Port more integration tests from Android. * Address feedback. * Update Quadruple library and re-enable tests (passing now). * Fix FIRDecimal128Value.isEqual to handle -/+0.
1 parent fe87384 commit 8a4075e

30 files changed

+2843
-151
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
#import <FirebaseFirestoreInternal/FIRDecimal128Value.h>

Firestore/CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Unreleased
22
- [feature] Adds support for the following new types: `MinKey`, `MaxKey`, `RegexValue`,
3-
`Int32Value`, `BSONObjectId`, `BSONTimestamp`, and `BSONBinaryData`. (#14800)
3+
`Int32Value`, `Decimal128Value`, `BSONObjectId`, `BSONTimestamp`, and `BSONBinaryData`. (#14800)
44

55
# 11.12.0
66
- [fixed] Fixed the `null` value handling in `isNotEqualTo` and `notIn` filters.

Firestore/Example/Tests/API/FIRBsonTypesUnitTests.mm

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#import <FirebaseFirestore/FIRBSONBinaryData.h>
1818
#import <FirebaseFirestore/FIRBSONObjectId.h>
1919
#import <FirebaseFirestore/FIRBSONTimestamp.h>
20+
#import <FirebaseFirestore/FIRDecimal128Value.h>
2021
#import <FirebaseFirestore/FIRFieldValue.h>
2122
#import <FirebaseFirestore/FIRInt32Value.h>
2223
#import <FirebaseFirestore/FIRMaxKey.h>
@@ -75,6 +76,39 @@ - (void)testCreateAndReadAndCompareInt32Value {
7576
XCTAssertFalse([val1 isEqual:val3]);
7677
}
7778

79+
- (void)testCreateAndReadAndCompareDecimal128Value {
80+
FIRDecimal128Value *val1 = [[FIRDecimal128Value alloc] initWithValue:@"1.2e3"];
81+
FIRDecimal128Value *val2 = [[FIRDecimal128Value alloc] initWithValue:@"12e2"];
82+
FIRDecimal128Value *val3 = [[FIRDecimal128Value alloc] initWithValue:@"0.12e4"];
83+
FIRDecimal128Value *val4 = [[FIRDecimal128Value alloc] initWithValue:@"12000e-1"];
84+
FIRDecimal128Value *val5 = [[FIRDecimal128Value alloc] initWithValue:@"1.2"];
85+
FIRDecimal128Value *val6 = [[FIRDecimal128Value alloc] initWithValue:@"NaN"];
86+
FIRDecimal128Value *val7 = [[FIRDecimal128Value alloc] initWithValue:@"Infinity"];
87+
FIRDecimal128Value *val8 = [[FIRDecimal128Value alloc] initWithValue:@"-Infinity"];
88+
FIRDecimal128Value *val9 = [[FIRDecimal128Value alloc] initWithValue:@"NaN"];
89+
FIRDecimal128Value *val10 = [[FIRDecimal128Value alloc] initWithValue:@"-0"];
90+
FIRDecimal128Value *val11 = [[FIRDecimal128Value alloc] initWithValue:@"0"];
91+
FIRDecimal128Value *val12 = [[FIRDecimal128Value alloc] initWithValue:@"-0.0"];
92+
FIRDecimal128Value *val13 = [[FIRDecimal128Value alloc] initWithValue:@"0.0"];
93+
94+
// Test reading the value back
95+
XCTAssertEqual(@"1.2e3", val1.value);
96+
97+
// Test isEqual
98+
XCTAssertTrue([val1 isEqual:val2]);
99+
XCTAssertTrue([val1 isEqual:val3]);
100+
XCTAssertTrue([val1 isEqual:val4]);
101+
XCTAssertFalse([val1 isEqual:val5]);
102+
103+
// Test isEqual for special values.
104+
XCTAssertTrue([val6 isEqual:val9]);
105+
XCTAssertFalse([val7 isEqual:val8]);
106+
XCTAssertFalse([val7 isEqual:val9]);
107+
XCTAssertTrue([val10 isEqual:val11]);
108+
XCTAssertTrue([val10 isEqual:val12]);
109+
XCTAssertTrue([val10 isEqual:val13]);
110+
}
111+
78112
- (void)testCreateAndReadAndCompareBsonObjectId {
79113
FIRBSONObjectId *val1 = [[FIRBSONObjectId alloc] initWithValue:@"abcd"];
80114
FIRBSONObjectId *val2 = [[FIRBSONObjectId alloc] initWithValue:@"abcd"];
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright 2025 Google LLC
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+
17+
#include "Firestore/Source/Public/FirebaseFirestore/FIRDecimal128Value.h"
18+
19+
#include "Firestore/core/src/util/quadruple.h"
20+
#include "Firestore/core/src/util/string_apple.h"
21+
22+
using firebase::firestore::util::MakeString;
23+
using firebase::firestore::util::Quadruple;
24+
25+
@implementation FIRDecimal128Value
26+
27+
- (instancetype)initWithValue:(NSString *)value {
28+
self = [super init];
29+
if (self) {
30+
_value = [value copy];
31+
}
32+
return self;
33+
}
34+
35+
- (BOOL)isEqual:(nullable id)object {
36+
if (self == object) {
37+
return YES;
38+
}
39+
40+
if (![object isKindOfClass:[FIRDecimal128Value class]]) {
41+
return NO;
42+
}
43+
44+
FIRDecimal128Value *other = (FIRDecimal128Value *)object;
45+
46+
Quadruple lhs = Quadruple();
47+
Quadruple rhs = Quadruple();
48+
lhs.Parse(MakeString(self.value));
49+
rhs.Parse(MakeString(other.value));
50+
51+
// Firestore considers +0 and -0 to be equal, but `Quadruple::Compare()` does not.
52+
if (lhs.Compare(Quadruple(-0.0)) == 0) lhs = Quadruple();
53+
if (rhs.Compare(Quadruple(-0.0)) == 0) rhs = Quadruple();
54+
55+
return lhs.Compare(rhs) == 0;
56+
}
57+
58+
- (id)copyWithZone:(__unused NSZone *_Nullable)zone {
59+
return [[FIRDecimal128Value alloc] initWithValue:self.value];
60+
}
61+
62+
- (NSString *)description {
63+
return [NSString stringWithFormat:@"<FIRDecimal128Value: (%@)>", self.value];
64+
}
65+
66+
@end

Firestore/Source/API/FSTUserDataReader.mm

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
#import "FIRBSONBinaryData.h"
2828
#import "FIRBSONObjectId.h"
2929
#import "FIRBSONTimestamp.h"
30+
#import "FIRDecimal128Value.h"
3031
#import "FIRGeoPoint.h"
3132
#import "FIRInt32Value.h"
3233
#import "FIRMaxKey.h"
@@ -452,6 +453,20 @@ - (ParsedUpdateData)parsedUpdateData:(id)input {
452453
return std::move(result);
453454
}
454455

456+
- (Message<google_firestore_v1_Value>)parseDecimal128Value:(FIRDecimal128Value *)decimal128
457+
context:(ParseContext &&)context {
458+
__block Message<google_firestore_v1_Value> result;
459+
result->which_value_type = google_firestore_v1_Value_map_value_tag;
460+
result->map_value = {};
461+
result->map_value.fields_count = 1;
462+
result->map_value.fields = nanopb::MakeArray<google_firestore_v1_MapValue_FieldsEntry>(1);
463+
result->map_value.fields[0].key = nanopb::CopyBytesArray(model::kDecimal128TypeFieldValue);
464+
result->map_value.fields[0].value =
465+
*[self encodeStringValue:MakeString(decimal128.value)].release();
466+
467+
return std::move(result);
468+
}
469+
455470
- (Message<google_firestore_v1_Value>)parseBsonObjectId:(FIRBSONObjectId *)oid
456471
context:(ParseContext &&)context {
457472
__block Message<google_firestore_v1_Value> result;
@@ -723,6 +738,9 @@ - (void)parseSentinelFieldValue:(FIRFieldValue *)fieldValue context:(ParseContex
723738
} else if ([input isKindOfClass:[FIRInt32Value class]]) {
724739
FIRInt32Value *value = input;
725740
return [self parseInt32Value:value context:std::move(context)];
741+
} else if ([input isKindOfClass:[FIRDecimal128Value class]]) {
742+
FIRDecimal128Value *value = input;
743+
return [self parseDecimal128Value:value context:std::move(context)];
726744
} else if ([input isKindOfClass:[FIRBSONObjectId class]]) {
727745
FIRBSONObjectId *oid = input;
728746
return [self parseBsonObjectId:oid context:std::move(context)];

Firestore/Source/API/FSTUserDataWriter.mm

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
#include "Firestore/Source/Public/FirebaseFirestore/FIRBSONBinaryData.h"
2727
#include "Firestore/Source/Public/FirebaseFirestore/FIRBSONObjectId.h"
2828
#include "Firestore/Source/Public/FirebaseFirestore/FIRBSONTimestamp.h"
29+
#include "Firestore/Source/Public/FirebaseFirestore/FIRDecimal128Value.h"
2930
#include "Firestore/Source/Public/FirebaseFirestore/FIRInt32Value.h"
3031
#include "Firestore/Source/Public/FirebaseFirestore/FIRMaxKey.h"
3132
#include "Firestore/Source/Public/FirebaseFirestore/FIRMinKey.h"
@@ -58,6 +59,8 @@
5859
using firebase::firestore::google_protobuf_Timestamp;
5960
using firebase::firestore::model::kRawBsonTimestampTypeIncrementFieldValue;
6061
using firebase::firestore::model::kRawBsonTimestampTypeSecondsFieldValue;
62+
using firebase::firestore::model::kRawDecimal128TypeFieldValue;
63+
using firebase::firestore::model::kRawInt32TypeFieldValue;
6164
using firebase::firestore::model::kRawRegexTypeOptionsFieldValue;
6265
using firebase::firestore::model::kRawRegexTypePatternFieldValue;
6366
using firebase::firestore::model::kRawVectorValueFieldKey;
@@ -109,7 +112,12 @@ - (id)convertedValue:(const google_firestore_v1_Value &)value {
109112
return value.boolean_value ? @YES : @NO;
110113
case TypeOrder::kNumber:
111114
if (value.which_value_type == google_firestore_v1_Value_map_value_tag) {
112-
return [self convertedInt32:value.map_value];
115+
absl::string_view key = MakeStringView(value.map_value.fields[0].key);
116+
if (key.compare(absl::string_view(kRawInt32TypeFieldValue)) == 0) {
117+
return [self convertedInt32:value.map_value];
118+
} else if (key.compare(absl::string_view(kRawDecimal128TypeFieldValue)) == 0) {
119+
return [self convertedDecimal128Value:value.map_value];
120+
}
113121
}
114122
return value.which_value_type == google_firestore_v1_Value_integer_value_tag
115123
? @(value.integer_value)
@@ -157,7 +165,7 @@ - (FIRVectorValue *)convertedVector:(const google_firestore_v1_MapValue &)mapVal
157165
for (pb_size_t i = 0; i < mapValue.fields_count; ++i) {
158166
absl::string_view key = MakeStringView(mapValue.fields[i].key);
159167
const google_firestore_v1_Value &value = mapValue.fields[i].value;
160-
if ((0 == key.compare(absl::string_view(kRawVectorValueFieldKey))) &&
168+
if ((key.compare(absl::string_view(kRawVectorValueFieldKey)) == 0) &&
161169
value.which_value_type == google_firestore_v1_Value_array_value_tag) {
162170
return [FIRFieldValue vectorWithArray:[self convertedArray:value.array_value]];
163171
}
@@ -174,11 +182,11 @@ - (FIRRegexValue *)convertedRegex:(const google_firestore_v1_MapValue &)mapValue
174182
for (pb_size_t i = 0; i < innerValue.map_value.fields_count; ++i) {
175183
absl::string_view key = MakeStringView(innerValue.map_value.fields[i].key);
176184
const google_firestore_v1_Value &value = innerValue.map_value.fields[i].value;
177-
if ((0 == key.compare(absl::string_view(kRawRegexTypePatternFieldValue))) &&
185+
if ((key.compare(absl::string_view(kRawRegexTypePatternFieldValue)) == 0) &&
178186
value.which_value_type == google_firestore_v1_Value_string_value_tag) {
179187
pattern = MakeNSString(MakeStringView(value.string_value));
180188
}
181-
if ((0 == key.compare(absl::string_view(kRawRegexTypeOptionsFieldValue))) &&
189+
if ((key.compare(absl::string_view(kRawRegexTypeOptionsFieldValue)) == 0) &&
182190
value.which_value_type == google_firestore_v1_Value_string_value_tag) {
183191
options = MakeNSString(MakeStringView(value.string_value));
184192
}
@@ -198,6 +206,18 @@ - (FIRInt32Value *)convertedInt32:(const google_firestore_v1_MapValue &)mapValue
198206
return [[FIRInt32Value alloc] initWithValue:value];
199207
}
200208

209+
- (FIRDecimal128Value *)convertedDecimal128Value:(const google_firestore_v1_MapValue &)mapValue {
210+
NSString *decimalString = @"";
211+
if (mapValue.fields_count == 1) {
212+
const google_firestore_v1_Value &decimalValue = mapValue.fields[0].value;
213+
if (decimalValue.which_value_type == google_firestore_v1_Value_string_value_tag) {
214+
decimalString = MakeNSString(MakeStringView(decimalValue.string_value));
215+
}
216+
}
217+
218+
return [[FIRDecimal128Value alloc] initWithValue:decimalString];
219+
}
220+
201221
- (FIRBSONObjectId *)convertedBsonObjectId:(const google_firestore_v1_MapValue &)mapValue {
202222
NSString *oid = @"";
203223
if (mapValue.fields_count == 1) {
@@ -219,12 +239,12 @@ - (FIRBSONTimestamp *)convertedBsonTimestamp:(const google_firestore_v1_MapValue
219239
for (pb_size_t i = 0; i < innerValue.map_value.fields_count; ++i) {
220240
absl::string_view key = MakeStringView(innerValue.map_value.fields[i].key);
221241
const google_firestore_v1_Value &value = innerValue.map_value.fields[i].value;
222-
if ((0 == key.compare(absl::string_view(kRawBsonTimestampTypeSecondsFieldValue))) &&
242+
if ((key.compare(absl::string_view(kRawBsonTimestampTypeSecondsFieldValue)) == 0) &&
223243
value.which_value_type == google_firestore_v1_Value_integer_value_tag) {
224244
// The value from the server is guaranteed to fit in a 32-bit unsigned integer.
225245
seconds = static_cast<uint32_t>(value.integer_value);
226246
}
227-
if ((0 == key.compare(absl::string_view(kRawBsonTimestampTypeIncrementFieldValue))) &&
247+
if ((key.compare(absl::string_view(kRawBsonTimestampTypeIncrementFieldValue)) == 0) &&
228248
value.which_value_type == google_firestore_v1_Value_integer_value_tag) {
229249
// The value from the server is guaranteed to fit in a 32-bit unsigned integer.
230250
increment = static_cast<uint32_t>(value.integer_value);
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Copyright 2025 Google LLC
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+
17+
#import <Foundation/Foundation.h>
18+
19+
NS_ASSUME_NONNULL_BEGIN
20+
21+
/**
22+
* Represents a 128-bit decimal number type in Firestore documents.
23+
*/
24+
NS_SWIFT_SENDABLE
25+
NS_SWIFT_NAME(Decimal128Value)
26+
__attribute__((objc_subclassing_restricted))
27+
@interface FIRDecimal128Value : NSObject<NSCopying>
28+
29+
/** The string representation of the 128-bit decimal value. */
30+
@property(nonatomic, copy, readonly) NSString *value;
31+
32+
/** :nodoc: */
33+
- (instancetype)init NS_UNAVAILABLE;
34+
35+
/**
36+
* Creates a `Decimal128Value` with the given value.
37+
* @param value The string representation of the number to be stored.
38+
*/
39+
- (instancetype)initWithValue:(NSString *)value NS_SWIFT_NAME(init(_:));
40+
41+
/** Returns true if the given object is equal to this, and false otherwise. */
42+
- (BOOL)isEqual:(nullable id)object;
43+
44+
@end
45+
46+
NS_ASSUME_NONNULL_END

Firestore/Swift/Source/Codable/CodablePassThroughTypes.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ struct FirestorePassthroughTypes: StructureCodingPassthroughTypeResolver {
3737
t is MaxKey ||
3838
t is RegexValue ||
3939
t is Int32Value ||
40+
t is Decimal128Value ||
4041
t is BSONObjectId ||
4142
t is BSONTimestamp ||
4243
t is BSONBinaryData
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* Copyright 2025 Google LLC
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+
17+
#if SWIFT_PACKAGE
18+
@_exported import FirebaseFirestoreInternalWrapper
19+
#else
20+
@_exported import FirebaseFirestoreInternal
21+
#endif // SWIFT_PACKAGE
22+
23+
/**
24+
* A protocol describing the encodable properties of a Decimal128Value.
25+
*
26+
* Note: this protocol exists as a workaround for the Swift compiler: if the Decimal128Value class
27+
* was extended directly to conform to Codable, the methods implementing the protocol would be need
28+
* to be marked required but that can't be done in an extension. Declaring the extension on the
29+
* protocol sidesteps this issue.
30+
*/
31+
private protocol CodableDecimal128Value: Codable {
32+
var value: String { get }
33+
34+
init(_ value: String)
35+
}
36+
37+
/** The keys in a Decimal128Value. Must match the properties of Decimal128Value. */
38+
private enum Decimal128ValueKeys: String, CodingKey {
39+
case value
40+
}
41+
42+
/**
43+
* An extension of Decimal128Value that implements the behavior of the Codable protocol.
44+
*
45+
* Note: this is implemented manually here because the Swift compiler can't synthesize these methods
46+
* when declaring an extension to conform to Codable.
47+
*/
48+
extension CodableDecimal128Value {
49+
public init(from decoder: Decoder) throws {
50+
let container = try decoder.container(keyedBy: Decimal128ValueKeys.self)
51+
let value = try container.decode(String.self, forKey: .value)
52+
self.init(value)
53+
}
54+
55+
public func encode(to encoder: Encoder) throws {
56+
var container = encoder.container(keyedBy: Decimal128ValueKeys.self)
57+
try container.encode(value, forKey: .value)
58+
}
59+
}
60+
61+
/** Extends Decimal128Value to conform to Codable. */
62+
extension FirebaseFirestore.Decimal128Value: FirebaseFirestore.CodableDecimal128Value {}

0 commit comments

Comments
 (0)